diff --git a/public/locales/en.json b/public/locales/en.json index 5679bd44..59026771 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -10,6 +10,9 @@ "username": "Username" }, "form-dialog": { + "confirmation_body": "{{context}}", + "confirmation_heading": "Are you sure you want to do this action?", + "confirmation_title": "Confirmation", "confirmation_body_add-source": "{{context}}", "confirmation_body_add-source_edit": "{{context}}", "confirmation_heading_add-source": "Are you sure you want to cancel adding this source?", @@ -20,6 +23,10 @@ "confirmation_body_add-source_exit_edit": "The wizard is in a pending state and will continue updating this source.", "confirmation_heading_add-source_exit": "Are you sure you want to exit this wizard?", "confirmation_title_add-source_exit": "Exit Wizard", + "confirmation_heading_delete-credential": " Are you sure you want to delete the credential <0>{{name}}?", + "confirmation_heading_delete-credential_other": " Are you sure you want to delete the following credentials?", + "confirmation_title_delete-credential": "Delete Credential", + "confirmation_title_delete-credential_other": "Delete Credentials", "confirmation_title_delete-source": "Delete Source", "confirmation_heading_delete-source": "Are you sure you want to delete the source <0>{{name}}?", "empty-state_description_add-source": "{{context}}", @@ -37,6 +44,7 @@ "label_add-credential": "Add a credential", "label_cancel": "Cancel", "label_close": "Close", + "label_confirm": "Confirm", "label_delete": "Delete", "label_no": "No", "label_option_disableSsl": "Disable SSL", @@ -75,6 +83,7 @@ "refreshed_load": "Refreshed {{refresh}}" }, "table": { + "header_auth-type": "Authorization type", "header_credentials": "Credentials", "header_description": "Description", "header_failed": "Failed systems", @@ -99,10 +108,17 @@ "label_action_scan_paused": "Resume scan", "label_action_scan_pending": "Cancel scan", "label_add": "Add", + "label_auth_cell": "Username and password", + "label_auth_cell_sshKey": "SSH Key", + "label_auth_tooltip": "Authorization type", "label_delete": "Delete", "label_edit": "Edit", "label_merge-reports": "Merge reports", + "label_network": "Network", + "label_satellite": "Satellite", "label_scan": "Scan", + "label_source": "Source", + "label_source_other": "Sources", "label_status": "{{context}}", "label_status_scans": "Scan created", "label_status_completed": "Last completed", @@ -147,6 +163,8 @@ "label_status_tooltip_success_sources_other": "{{count}} Successful Authentications", "label_status_tooltip_unreachable": "{{count}} Unreachable System", "label_status_tooltip_unreachable_other": "{{count}} Unreachable Systems", + "label_vcenter": "VCenter", + "label_view": "View", "tooltip_merge-reports": "Merge selected scan results into a single report" }, "toast-notifications": { @@ -155,6 +173,10 @@ "description_add-source_hidden_edit": "Source {{name}} was updated", "description_add-source_hidden_error": "{{message}}", "description_add-source_hidden_error_edit": "{{message}}", + "description_deleted-credential": "Deleted credential {{name}}", + "description_deleted-credential_other": "Deleted credentials {{name}} and more", + "description_deleted-credential_error": "Error removing credential {{name}}. {{message}}", + "description_deleted-credential_error_other": "Error removing credentials {{name}} and more. {{message}}", "description_deleted-source": "Deleted source {{name}}.", "description_error": "Application error", "description_scan-report_canceled": "Scan <0>{{name}} stopped", @@ -168,6 +190,8 @@ "title_add-source_hidden_edit": "Success updating source", "title_add-source_hidden_error": "Error creating source", "title_add-source_hidden_error_edit": "Error updating source", + "title_deleted-credential": "Success deleting credential", + "title_deleted-credential_other": "Success deleting credentials", "title_deleted-source": "Success deleting source", "title_error": "Error", "title_warning": "Warning" diff --git a/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap b/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap index 30f5b1ac..07fb1410 100644 --- a/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap +++ b/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap @@ -21,8 +21,40 @@ exports[`Confirmation Modal Component should allow custom content: custom 1`] = role="dialog" style="--pf-c-modal-box--m-align-top--spacer: 5%;" > -
- lorem ipsum +
+
+ +
+

+ + Warning alert: + + t(form-dialog.confirmation, {"context":"heading"}) +

@@ -66,13 +98,12 @@ exports[`Confirmation Modal Component should allow passed children, or specific > Warning alert: + t(form-dialog.confirmation, {"context":"heading"})
-

- Dolor sit -

+ Dolor sit
@@ -83,8 +114,40 @@ exports[`Confirmation Modal Component should allow passed children, or specific class="pf-c-modal-box__body" id="pf-modal-part-4" > -
- hello world +
+
+ +
+

+ + Warning alert: + + t(form-dialog.confirmation, {"context":"heading"}) +

`; @@ -128,11 +191,6 @@ exports[`Confirmation Modal Component should allow passed children, or specific Lorem ipsum -
-

-

`; @@ -233,9 +291,7 @@ exports[`Confirmation Modal Component should display a confirmation modal: show
-

- Test body -

+ Test body
diff --git a/src/components/confirmationModal/confirmationModal.js b/src/components/confirmationModal/confirmationModal.js index d0892dde..8aba38f9 100644 --- a/src/components/confirmationModal/confirmationModal.js +++ b/src/components/confirmationModal/confirmationModal.js @@ -70,24 +70,26 @@ const ConfirmationModal = ({ if (onConfirm) { actions.push( ); } actions.push( ); return actions; }; + const updatedHeading = heading || t('form-dialog.confirmation', { context: ['heading'] }); + const updatedChildren = - body || heading ? ( - -

{body}

+ body || updatedHeading ? ( + + {body} ) : ( children @@ -100,9 +102,7 @@ const ConfirmationModal = ({ className="quipucords-modal__confirmation" disableFocusTrap header={ - title && ( - {title || t('form-dialog.label', { context: ['submit', 'confirmation'] })} - ) + title && {title || t('form-dialog.confirmation', { context: ['title'] })} } isContentOnly={isContentOnly} isOpen={show} diff --git a/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js b/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js index ad9dda76..19a9ff2f 100644 --- a/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js +++ b/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js @@ -12,7 +12,7 @@ describe('CreateCredentialDialog Component', () => { const generateEmptyStore = (obj = {}) => configureMockStore()(obj); it('should render a connected component', () => { - const store = generateEmptyStore({ credentials: { update: { show: true } }, viewOptions: {} }); + const store = generateEmptyStore({ credentials: { dialog: { show: true } }, viewOptions: {} }); const component = mount( diff --git a/src/components/createCredentialDialog/createCredentialDialog.js b/src/components/createCredentialDialog/createCredentialDialog.js index 468df51f..35ee444e 100644 --- a/src/components/createCredentialDialog/createCredentialDialog.js +++ b/src/components/createCredentialDialog/createCredentialDialog.js @@ -535,13 +535,13 @@ CreateCredentialDialog.defaultProps = { }; const mapDispatchToProps = dispatch => ({ - getCredentials: queryObj => dispatch(reduxActions.credentials.getCredentials(queryObj)), + getCredentials: queryObj => dispatch(reduxActions.credentials.getCredentials(null, queryObj)), addCredential: data => dispatch(reduxActions.credentials.addCredential(data)), updateCredential: (id, data) => dispatch(reduxActions.credentials.updateCredential(id, data)) }); const mapStateToProps = state => ({ - ...state.credentials.update, + ...state.credentials.dialog, viewOptions: state.viewOptions[reduxTypes.view.CREDENTIALS_VIEW] }); diff --git a/src/components/credentials/__tests__/__snapshots__/credentials.test.js.snap b/src/components/credentials/__tests__/__snapshots__/credentials.test.js.snap new file mode 100644 index 00000000..57cd5732 --- /dev/null +++ b/src/components/credentials/__tests__/__snapshots__/credentials.test.js.snap @@ -0,0 +1,530 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Credentials Component should handle multiple display states, pending, error, fulfilled: error 1`] = ` + + + t(view.error-message, {"context":"credentials"}) + + +`; + +exports[`Credentials Component should handle multiple display states, pending, error, fulfilled: fulfilled 1`] = ` +
+ + + + + + } + activeFilters={Array []} + filterFields={ + Array [ + Object { + "filterType": "text", + "id": "search_by_name", + "placeholder": "Filter by Name", + "title": "Name", + }, + Object { + "filterType": "select", + "filterValues": Array [ + Object { + "id": "network", + "title": "Network", + }, + Object { + "id": "satellite", + "title": "Satellite", + }, + Object { + "id": "vcenter", + "title": "VCenter", + }, + ], + "id": "cred_type", + "placeholder": "Filter by Credential Type", + "title": "Credential Type", + }, + ] + } + filterType={Object {}} + filterValue="" + itemsType="Credential" + itemsTypePlural="Credentials" + lastRefresh={NaN} + onRefresh={[Function]} + selectedCount={0} + sortAscending={true} + sortFields={ + Array [ + Object { + "id": "name", + "isNumeric": false, + "title": "Name", + }, + Object { + "id": "cred_type", + "isNumeric": false, + "title": "Credential Type", + }, + ] + } + sortType={Object {}} + totalCount={0} + viewType="CREDENTIALS_VIEW" + /> + +
+ + + + + + + +
+ + lorem + +
+
+ , + "dataLabel": "t(table.header, {\\"context\\":\\"description\\"})", + "width": 35, + }, + Object { + "content": + t(table.label_auth_cell_sshKey, {"context":"credentials"}) + , + "dataLabel": "t(table.header, {\\"context\\":\\"auth-type\\"})", + }, + Object { + "content": + t(table.label_status_cell, {"context":"credentials","count":0}, [object Object],[object Object]) + , + "dataLabel": "t(table.header, {\\"context\\":\\"sources\\"})", + "expandedContent": undefined, + "isExpanded": false, + "width": 8, + }, + Object { + "content": + + + + + + + + + + + + + + + + + } + position="right" + selectedOptions={null} + splitButtonVariant={null} + toggleIcon={null} + variant="single" + /> + + , + "isActionCell": true, + "style": Object { + "textAlign": "right", + }, + }, + ], + "isSelected": false, + "item": Object { + "id": "1", + "name": "lorem", + }, + }, + ] + } + summary={null} + variant="compact" + > + +
+
+
+`; + +exports[`Credentials Component should handle multiple display states, pending, error, fulfilled: pending 1`] = ` + + +
+ t(view.loading, {"context":"credentials"}) +
+
+`; + +exports[`Credentials Component should render a basic component: basic 1`] = ` +
+
+ + +
+
+
+`; + +exports[`Credentials Component should return an empty state when there are no credentials: empty state 1`] = ` +
+
+
+
+
+
+ +

+ t(view.empty-state, {"context":"title","name":"Quipucords"}) +

+
+ t(view.empty-state_description, {"context":"credentials","name":"The Quipucords tool"}) +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+`; diff --git a/src/components/credentials/__tests__/__snapshots__/credentialsConstants.test.js.snap b/src/components/credentials/__tests__/__snapshots__/credentialsConstants.test.js.snap new file mode 100644 index 00000000..6127c791 --- /dev/null +++ b/src/components/credentials/__tests__/__snapshots__/credentialsConstants.test.js.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CredentialTypes should have specific CredentialFilterFields properties: CredentialFilterFields 1`] = ` +Array [ + Object { + "filterType": "text", + "id": "search_by_name", + "placeholder": "Filter by Name", + "title": "Name", + }, + Object { + "filterType": "select", + "filterValues": Array [ + Object { + "id": "network", + "title": "Network", + }, + Object { + "id": "satellite", + "title": "Satellite", + }, + Object { + "id": "vcenter", + "title": "VCenter", + }, + ], + "id": "cred_type", + "placeholder": "Filter by Credential Type", + "title": "Credential Type", + }, +] +`; + +exports[`CredentialTypes should have specific CredentialSortFields properties: CredentialSortFields 1`] = ` +Array [ + Object { + "id": "name", + "isNumeric": false, + "title": "Name", + }, + Object { + "id": "cred_type", + "isNumeric": false, + "title": "Credential Type", + }, +] +`; diff --git a/src/components/credentials/__tests__/__snapshots__/credentialsContext.test.js.snap b/src/components/credentials/__tests__/__snapshots__/credentialsContext.test.js.snap new file mode 100644 index 00000000..1f5f8d5d --- /dev/null +++ b/src/components/credentials/__tests__/__snapshots__/credentialsContext.test.js.snap @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CredentialsContext should apply a hook for retrieving data from multiple selectors: responses 1`] = ` +Object { + "errorResponse": Object { + "data": Array [], + "date": undefined, + "error": true, + "errorMessage": "Lorem ipsum", + "expandedRows": undefined, + "fulfilled": undefined, + "pending": undefined, + "selectedRows": undefined, + }, + "fulfilledResponse": Object { + "data": Array [ + "dolor", + "sit", + ], + "date": undefined, + "error": undefined, + "errorMessage": undefined, + "expandedRows": undefined, + "fulfilled": true, + "pending": undefined, + "selectedRows": undefined, + }, + "mockStoreSuccessResponse": Object { + "data": Array [ + "lorem", + "ipsum", + ], + "date": undefined, + "error": false, + "errorMessage": null, + "expandedRows": Object {}, + "fulfilled": true, + "pending": false, + "selectedRows": Object {}, + }, + "pendingResponse": Object { + "data": Array [], + "date": undefined, + "error": undefined, + "errorMessage": undefined, + "expandedRows": undefined, + "fulfilled": undefined, + "pending": true, + "selectedRows": undefined, + }, +} +`; + +exports[`CredentialsContext should handle credential actions for onEdit: dispatch onEdit 1`] = ` +Array [ + Array [ + Object { + "credential": Object { + "id": "lorem ipsum base id", + "name": "lorem ipsum name", + }, + "type": "EDIT_CREDENTIAL_SHOW", + }, + ], +] +`; + +exports[`CredentialsContext should handle deleting a credential with a confirmation: dispatch onDelete 1`] = ` +Array [ + Array [ + Object { + "body": undefined, + "confirmButtonText": "t(form-dialog.label, {\\"context\\":\\"delete\\"})", + "heading": "t(form-dialog.confirmation_heading, {\\"context\\":\\"delete-credential\\",\\"count\\":1,\\"name\\":\\"lorem ipsum name\\"}, [object Object])", + "onConfirm": [Function], + "title": "t(form-dialog.confirmation_title, {\\"context\\":\\"delete-credential\\",\\"count\\":1})", + }, + ], +] +`; + +exports[`CredentialsContext should return specific properties: specific properties 1`] = ` +Object { + "useGetCredentials": [Function], + "useOnDelete": [Function], + "useOnEdit": [Function], + "useOnExpand": [Function], + "useOnRefresh": [Function], + "useOnSelect": [Function], +} +`; diff --git a/src/components/credentials/__tests__/__snapshots__/credentialsEmptyState.test.js.snap b/src/components/credentials/__tests__/__snapshots__/credentialsEmptyState.test.js.snap index fafa5379..2fa15e2a 100644 --- a/src/components/credentials/__tests__/__snapshots__/credentialsEmptyState.test.js.snap +++ b/src/components/credentials/__tests__/__snapshots__/credentialsEmptyState.test.js.snap @@ -32,7 +32,7 @@ exports[`CredentialsEmptyState Component should render a basic component 1`] = `
- t(view.empty-state_description, {"context":"credentials","name":"The Quipucords tool"}) + t(view.empty-state, {"context":"description","name":"The Quipucords tool"})
- t(view.empty-state_label, {"context":"source"}) + t(view.empty-state_label, {"context":"sources"})
diff --git a/src/components/credentials/__tests__/__snapshots__/credentialsListItem.test.js.snap b/src/components/credentials/__tests__/__snapshots__/credentialsListItem.test.js.snap deleted file mode 100644 index 23e4eb97..00000000 --- a/src/components/credentials/__tests__/__snapshots__/credentialsListItem.test.js.snap +++ /dev/null @@ -1,945 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CredentialListItem Component should render a connected component: connected 1`] = ` - -`; - -exports[`CredentialListItem Component should render a non-connected component: non-connected 1`] = ` - - - - , - - - , - ] - } - additionalInfo={ - Array [ - - - - 0 - - Sources - - , - ] - } - checkboxInput={ - - } - className="quipucords-credential-list-item list-view-pf-top-align" - compoundExpand={true} - compoundExpanded={false} - description={ -
- - - - - - Username and Password - - -
- } - heading={null} - hideCloseIcon={false} - initExpanded={false} - key="1" - leftContent={ - - - - } - onCloseCompoundExpand={[Function]} - onExpand={[Function]} - onExpandClose={[Function]} - stacked={true} - > - -
- - - , - - - , - ] - } - additionalInfo={ - Array [ - - - - 0 - - Sources - - , - ] - } - checkboxInput={ - - } - description={ -
- - - - - - Username and Password - - -
- } - heading={null} - leftContent={ - - - - } - > - -
- -
- -
-
-
-
- -
- - - - - - View Credential - -
- } - popperMatchesTriggerWidth={false} - positionModifiers={ - Object { - "bottom": "pf-m-bottom", - "bottom-end": "pf-m-bottom-right", - "bottom-start": "pf-m-bottom-left", - "left": "pf-m-left", - "left-end": "pf-m-left-bottom", - "left-start": "pf-m-left-top", - "right": "pf-m-right", - "right-end": "pf-m-right-bottom", - "right-start": "pf-m-right-top", - "top": "pf-m-top", - "top-end": "pf-m-top-right", - "top-start": "pf-m-top-left", - } - } - trigger={ -
- -
- } - zIndex={9999} - > - -
- - - -
-
- - - - - - - - - Delete Credential - -
- } - popperMatchesTriggerWidth={false} - positionModifiers={ - Object { - "bottom": "pf-m-bottom", - "bottom-end": "pf-m-bottom-right", - "bottom-start": "pf-m-bottom-left", - "left": "pf-m-left", - "left-end": "pf-m-left-bottom", - "left-start": "pf-m-left-top", - "right": "pf-m-right", - "right-end": "pf-m-right-bottom", - "right-start": "pf-m-right-top", - "top": "pf-m-top", - "top-end": "pf-m-top-right", - "top-start": "pf-m-top-left", - } - } - trigger={ -
- -
- } - zIndex={9999} - > - -
- - - -
-
- - - - - - -
- -
- - - - - - Network - -
- } - popperMatchesTriggerWidth={false} - positionModifiers={ - Object { - "bottom": "pf-m-bottom", - "bottom-end": "pf-m-bottom-right", - "bottom-start": "pf-m-bottom-left", - "left": "pf-m-left", - "left-end": "pf-m-left-bottom", - "left-start": "pf-m-left-top", - "right": "pf-m-right", - "right-end": "pf-m-right-bottom", - "right-start": "pf-m-right-top", - "top": "pf-m-top", - "top-end": "pf-m-top-right", - "top-start": "pf-m-top-left", - } - } - trigger={ -
- -
- } - zIndex={9999} - > - -
- - - - - - -
-
- - - -
- - -
- -
- -
-
- - -
- - - - - - - - - Authorization Type - -
- } - popperMatchesTriggerWidth={false} - positionModifiers={ - Object { - "bottom": "pf-m-bottom", - "bottom-end": "pf-m-bottom-right", - "bottom-start": "pf-m-bottom-left", - "left": "pf-m-left", - "left-end": "pf-m-left-bottom", - "left-start": "pf-m-left-top", - "right": "pf-m-right", - "right-end": "pf-m-right-bottom", - "right-start": "pf-m-right-top", - "top": "pf-m-top", - "top-end": "pf-m-top-right", - "top-start": "pf-m-top-left", - } - } - trigger={ -
- Username and Password -
- } - zIndex={9999} - > - -
- Username and Password -
-
- - - -
-
-
-
-
-
- -
- -
- -
- - - 0 - - Sources -
-
-
-
-
-
-
-
- -
- - -
-
-
-`; diff --git a/src/components/credentials/__tests__/__snapshots__/credentialsTableCells.test.js.snap b/src/components/credentials/__tests__/__snapshots__/credentialsTableCells.test.js.snap new file mode 100644 index 00000000..905d5a98 --- /dev/null +++ b/src/components/credentials/__tests__/__snapshots__/credentialsTableCells.test.js.snap @@ -0,0 +1,184 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CredentialsTableCells should export specific function components: function components 1`] = ` +Object { + "actionsCell": [Function], + "authType": [Function], + "description": [Function], + "sourcesCellContent": [Function], + "sourcesContent": [Function], + "statusCell": [Function], +} +`; + +exports[`CredentialsTableCells should return consistent cell results: basic actionsCell cell 1`] = ` + + + + + + + + + + + + + + + + + + } + position="right" + selectedOptions={null} + splitButtonVariant={null} + toggleIcon={null} + variant="single" + /> + + +`; + +exports[`CredentialsTableCells should return consistent cell results: basic authType cell 1`] = ` + + t(table.label_auth_cell, {"context":"sshKey"}) + +`; + +exports[`CredentialsTableCells should return consistent cell results: basic description cell 1`] = ` + + + + + + + +
+ +
+
+
+`; + +exports[`CredentialsTableCells should return consistent cell results: basic sourcesCellContent cell 1`] = ` +Object { + "content": + t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object]) + , + "expandedContent": undefined, +} +`; + +exports[`CredentialsTableCells should return consistent cell results: basic sourcesContent cell 1`] = ` + +`; + +exports[`CredentialsTableCells should return consistent cell results: basic statusCell cell 1`] = ` + + t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object]) + +`; diff --git a/src/components/credentials/__tests__/credentials.test.js b/src/components/credentials/__tests__/credentials.test.js new file mode 100644 index 00000000..a0b45537 --- /dev/null +++ b/src/components/credentials/__tests__/credentials.test.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { Credentials } from '../credentials'; +import { apiTypes } from '../../../constants/apiConstants'; + +describe('Credentials Component', () => { + it('should render a basic component', async () => { + const props = { + useGetCredentials: () => ({ + fulfilled: true + }) + }; + + const component = await shallowHookComponent(); + expect(component).toMatchSnapshot('basic'); + }); + + it('should handle multiple display states, pending, error, fulfilled', async () => { + const props = { + useGetCredentials: () => ({ + pending: true + }) + }; + + const component = await shallowHookComponent(); + expect(component).toMatchSnapshot('pending'); + + component.setProps({ + useGetCredentials: () => ({ + pending: false, + error: true + }) + }); + + expect(component).toMatchSnapshot('error'); + + component.setProps({ + useGetCredentials: () => ({ + pending: false, + error: false, + fulfilled: true, + data: [ + { + [apiTypes.API_RESPONSE_CREDENTIAL_ID]: '1', + [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: 'lorem' + } + ] + }) + }); + + expect(component).toMatchSnapshot('fulfilled'); + }); + + it('should return an empty state when there are no credentials', async () => { + const props = { + useGetCredentials: () => ({ + fulfilled: true, + data: [] + }) + }; + + const component = await shallowHookComponent(); + expect(component.render()).toMatchSnapshot('empty state'); + }); +}); diff --git a/src/components/credentials/__tests__/credentialsConstants.test.js b/src/components/credentials/__tests__/credentialsConstants.test.js new file mode 100644 index 00000000..7cd46522 --- /dev/null +++ b/src/components/credentials/__tests__/credentialsConstants.test.js @@ -0,0 +1,11 @@ +import { CredentialFilterFields, CredentialSortFields } from '../credentialConstants'; + +describe('CredentialTypes', () => { + it('should have specific CredentialFilterFields properties', () => { + expect(CredentialFilterFields).toMatchSnapshot('CredentialFilterFields'); + }); + + it('should have specific CredentialSortFields properties', () => { + expect(CredentialSortFields).toMatchSnapshot('CredentialSortFields'); + }); +}); diff --git a/src/components/credentials/__tests__/credentialsContext.test.js b/src/components/credentials/__tests__/credentialsContext.test.js new file mode 100644 index 00000000..a24a6462 --- /dev/null +++ b/src/components/credentials/__tests__/credentialsContext.test.js @@ -0,0 +1,89 @@ +import { context, useGetCredentials, useOnDelete, useOnEdit } from '../credentialsContext'; +import { apiTypes } from '../../../constants/apiConstants'; +import { reduxTypes } from '../../../redux'; + +describe('CredentialsContext', () => { + it('should return specific properties', () => { + expect(context).toMatchSnapshot('specific properties'); + }); + + it('should handle deleting a credential with a confirmation', async () => { + const mockConfirmation = jest.fn(); + const mockCredential = { + [apiTypes.API_RESPONSE_CREDENTIAL_ID]: 'lorem ipsum base id', + [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: 'lorem ipsum name' + }; + + const { result } = await shallowHook(() => + useOnDelete({ + useConfirmation: () => mockConfirmation + }) + ); + + result(mockCredential); + + expect(mockConfirmation.mock.calls).toMatchSnapshot('dispatch onDelete'); + mockConfirmation.mockClear(); + }); + + it('should handle credential actions for onEdit', async () => { + const mockDispatch = jest.fn(); + const mockCredential = { + [apiTypes.API_RESPONSE_CREDENTIAL_ID]: 'lorem ipsum base id', + [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: 'lorem ipsum name' + }; + + const { result } = await shallowHook(() => + useOnEdit({ + useDispatch: () => mockDispatch + }) + ); + + result(mockCredential); + + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch onEdit'); + mockDispatch.mockClear(); + }); + + it('should apply a hook for retrieving data from multiple selectors', () => { + const { result: errorResponse } = shallowHook(() => + useGetCredentials({ + useSelectorsResponse: () => ({ error: true, message: 'Lorem ipsum' }) + }) + ); + + const { result: pendingResponse } = shallowHook(() => + useGetCredentials({ + useSelectorsResponse: () => ({ pending: true }) + }) + ); + + const { result: fulfilledResponse } = shallowHook(() => + useGetCredentials({ + useSelectorsResponse: () => ({ fulfilled: true, data: { view: { results: ['dolor', 'sit'] } } }) + }) + ); + + const { result: mockStoreSuccessResponse } = shallowHook(() => useGetCredentials(), { + state: { + viewOptions: { + [reduxTypes.view.SCANS_VIEW]: {} + }, + credentials: { + expanded: {}, + selected: {}, + view: { + fulfilled: true, + data: { + results: ['lorem', 'ipsum'] + } + } + } + } + }); + + expect({ errorResponse, fulfilledResponse, pendingResponse, mockStoreSuccessResponse }).toMatchSnapshot( + 'responses' + ); + }); +}); diff --git a/src/components/credentials/__tests__/credentialsListItem.test.js b/src/components/credentials/__tests__/credentialsListItem.test.js deleted file mode 100644 index 1e61373b..00000000 --- a/src/components/credentials/__tests__/credentialsListItem.test.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import { Provider } from 'react-redux'; -import { mount, shallow } from 'enzyme'; -import { ConnectedCredentialListItem, CredentialListItem } from '../credentialListItem'; -import { viewTypes } from '../../../redux/constants'; - -describe('CredentialListItem Component', () => { - const generateEmptyStore = (obj = {}) => configureMockStore()(obj); - - it('should render a connected component', () => { - const store = generateEmptyStore({ credentials: {}, viewOptions: { [viewTypes.CREDENTIALS_VIEW]: {} } }); - - const props = { - item: { - id: 1, - cred_type: 'network' - } - }; - - const component = shallow( - - - - ); - expect(component.find(ConnectedCredentialListItem)).toMatchSnapshot('connected'); - }); - - it('should render a non-connected component', () => { - const props = { - item: { - id: 1, - cred_type: 'network' - } - }; - - const component = mount(); - expect(component).toMatchSnapshot('non-connected'); - }); -}); diff --git a/src/components/credentials/__tests__/credentialsTableCells.test.js b/src/components/credentials/__tests__/credentialsTableCells.test.js new file mode 100644 index 00000000..a3c8048c --- /dev/null +++ b/src/components/credentials/__tests__/credentialsTableCells.test.js @@ -0,0 +1,13 @@ +import { credentialsTableCells } from '../credentialsTableCells'; + +describe('CredentialsTableCells', () => { + it('should export specific function components', () => { + expect(credentialsTableCells).toMatchSnapshot('function components'); + }); + + it('should return consistent cell results', () => { + Object.entries(credentialsTableCells).forEach(([key, value]) => + expect(value()).toMatchSnapshot(`basic ${key} cell`) + ); + }); +}); diff --git a/src/components/credentials/credentialListItem.js b/src/components/credentials/credentialListItem.js deleted file mode 100644 index 1998aef8..00000000 --- a/src/components/credentials/credentialListItem.js +++ /dev/null @@ -1,213 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; -import { ListView, Icon, Checkbox } from 'patternfly-react'; -import { Button, ButtonVariant, List, ListItem } from '@patternfly/react-core'; -import { EyeIcon, TrashIcon } from '@patternfly/react-icons'; -import _find from 'lodash/find'; -import _get from 'lodash/get'; -import { connect, reduxTypes, store } from '../../redux'; -import { helpers } from '../../common/helpers'; -import { dictionary } from '../../constants/dictionaryConstants'; -import Tooltip from '../tooltip/tooltip'; - -class CredentialListItem extends React.Component { - static authType(item) { - return item.ssh_keyfile && item.ssh_keyfile !== '' ? 'sshKey' : 'usernamePassword'; - } - - onItemSelectChange = () => { - const { item } = this.props; - - store.dispatch({ - type: this.isSelected() ? reduxTypes.view.DESELECT_ITEM : reduxTypes.view.SELECT_ITEM, - viewType: reduxTypes.view.CREDENTIALS_VIEW, - item - }); - }; - - onToggleExpand = expandType => { - const { item } = this.props; - - if (expandType === this.expandType()) { - store.dispatch({ - type: reduxTypes.view.EXPAND_ITEM, - viewType: reduxTypes.view.CREDENTIALS_VIEW, - item - }); - } else { - store.dispatch({ - type: reduxTypes.view.EXPAND_ITEM, - viewType: reduxTypes.view.CREDENTIALS_VIEW, - item, - expandType - }); - } - }; - - onCloseExpand = () => { - const { item } = this.props; - store.dispatch({ - type: reduxTypes.view.EXPAND_ITEM, - viewType: reduxTypes.view.CREDENTIALS_VIEW, - item - }); - }; - - expandType() { - const { item, expandedCredentials } = this.props; - - return _get( - _find(expandedCredentials, nextExpanded => nextExpanded.id === item.id), - 'expandType' - ); - } - - isSelected() { - const { item, selectedCredentials } = this.props; - - return _find(selectedCredentials, nextSelected => nextSelected.id === item.id) !== undefined; - } - - renderActions() { - const { item, onEdit, onDelete } = this.props; - - return [ - - - , - - - - ]; - } - - renderStatusItems() { - const { item } = this.props; - - const sourceCount = item.sources ? item.sources.length : 0; - - return [ - - { - this.onToggleExpand('sources'); - }} - > - {sourceCount} - {sourceCount === 1 ? ' Source' : ' Sources'} - - - ]; - } - - renderExpansionContents() { - const { item, expandedCredentials } = this.props; - const typeIcon = helpers.sourceTypeIcon(item.cred_type); - - switch (this.expandType(item, expandedCredentials)) { - case 'sources': - (item.sources || []).sort((item1, item2) => item1.name.localeCompare(item2.name)); - return ( - - {item?.sources?.map(source => ( - }> - {source.name} - - ))} - - ); - default: - return null; - } - } - - render() { - const { item } = this.props; - const selected = this.isSelected(); - const sourceTypeIcon = helpers.sourceTypeIcon(item.cred_type); - - const leftContent = ( - - - - ); - - const description = ( -
- - {item.name} - - - {dictionary[CredentialListItem.authType(item)]} - -
- ); - - return ( - } - actions={this.renderActions()} - leftContent={leftContent} - description={description} - additionalInfo={this.renderStatusItems()} - compoundExpand - compoundExpanded={this.expandType() !== undefined} - onCloseCompoundExpand={this.onCloseExpand} - > - {this.renderExpansionContents()} - - ); - } -} - -CredentialListItem.propTypes = { - item: PropTypes.object.isRequired, - onEdit: PropTypes.func, - onDelete: PropTypes.func, - selectedCredentials: PropTypes.array, - expandedCredentials: PropTypes.array -}; - -CredentialListItem.defaultProps = { - onEdit: helpers.noop, - onDelete: helpers.noop, - selectedCredentials: [], - expandedCredentials: [] -}; - -const mapStateToProps = state => ({ - selectedCredentials: state.viewOptions[reduxTypes.view.CREDENTIALS_VIEW].selectedItems, - expandedCredentials: state.viewOptions[reduxTypes.view.CREDENTIALS_VIEW].expandedItems -}); - -const ConnectedCredentialListItem = connect(mapStateToProps)(CredentialListItem); - -export { ConnectedCredentialListItem as default, ConnectedCredentialListItem, CredentialListItem }; diff --git a/src/components/credentials/credentials.js b/src/components/credentials/credentials.js index 77c7b006..74b82212 100644 --- a/src/components/credentials/credentials.js +++ b/src/components/credentials/credentials.js @@ -1,364 +1,225 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; -import _get from 'lodash/get'; -import _isEqual from 'lodash/isEqual'; -import _size from 'lodash/size'; -import { - Alert, - AlertVariant, - Button, - ButtonVariant, - EmptyState, - EmptyStateBody, - EmptyStateIcon, - EmptyStatePrimary, - EmptyStateVariant, - Title, - TitleSizes -} from '@patternfly/react-core'; -import { SearchIcon } from '@patternfly/react-icons'; -import { Form, ListView } from 'patternfly-react'; +import { Alert, AlertVariant, Button, ButtonVariant, EmptyState, Spinner } from '@patternfly/react-core'; +import { IconSize } from '@patternfly/react-icons'; import { Modal, ModalVariant } from '../modal/modal'; import { AddCredentialType, ButtonVariant as CredentialButtonVariant, SelectPosition } from '../addCredentialType/addCredentialType'; -import { connect, reduxActions, reduxTypes, store } from '../../redux'; -import helpers from '../../common/helpers'; +import { reduxTypes, storeHooks } from '../../redux'; import ViewToolbar from '../viewToolbar/viewToolbar'; import ViewPaginationRow from '../viewPaginationRow/viewPaginationRow'; -import CredentialsEmptyState from './credentialsEmptyState'; -import CredentialListItem from './credentialListItem'; +import { CredentialsEmptyState } from './credentialsEmptyState'; import { CredentialFilterFields, CredentialSortFields } from './credentialConstants'; import { translate } from '../i18n/i18n'; - -class Credentials extends React.Component { - credentialsToDelete = []; - - deletingCredential = null; - - state = { - lastRefresh: null - }; - - componentDidMount() { - this.onRefresh(); - } - - UNSAFE_componentWillReceiveProps(nextProps) { //eslint-disable-line - const { credentials, fulfilled, update, viewOptions } = this.props; - - if (!_isEqual(nextProps.credentials, credentials) && nextProps.fulfilled && !fulfilled) { - this.setState({ lastRefresh: Date.now() }); - } - - // Check for changes resulting in a fetch - if (helpers.viewPropsChanged(nextProps.viewOptions, viewOptions)) { - this.onRefresh(nextProps); - } - - if (_get(nextProps, 'update.delete')) { - if (nextProps.update.fulfilled && !update.fulfilled) { - store.dispatch({ - type: reduxTypes.toastNotifications.TOAST_ADD, - alertType: 'success', - message: ( - - Credential {this.deletingCredential.name} successfully deleted. - - ) - }); - this.onRefresh(nextProps); - - store.dispatch({ - type: reduxTypes.view.DESELECT_ITEM, - viewType: reduxTypes.view.CREDENTIALS_VIEW, - item: this.deletingCredential - }); - - this.deleteNextCredential(); - } - - if (nextProps.update.error && !update.error) { - store.dispatch({ - type: reduxTypes.toastNotifications.TOAST_ADD, - alertType: 'danger', - header: 'Error', - message: ( - - Error removing credential {this.deletingCredential.name} -

{nextProps.update.errorMessage}

-
- ) - }); - - this.deleteNextCredential(); - } - } - } - - onDeleteCredentials = () => { - const { viewOptions } = this.props; - - if (viewOptions.selectedItems.length === 1) { - this.onDeleteCredential(viewOptions.selectedItems[0]); - return; - } - - const heading = Are you sure you want to delete the following credentials?; - - let credentialsList = ''; - viewOptions.selectedItems.forEach((item, index) => { - credentialsList += (index > 0 ? '\n' : '') + item.name; - }); - - const body = ( - - ); - - const onConfirm = () => this.doDeleteCredentials(viewOptions.selectedItems); - - store.dispatch({ - type: reduxTypes.confirmationModal.CONFIRMATION_MODAL_SHOW, - title: 'Delete Credentials', - heading, - body, - confirmButtonText: 'Delete', - onConfirm - }); - }; - - onEditCredential = item => { - store.dispatch({ - type: reduxTypes.credentials.EDIT_CREDENTIAL_SHOW, - credential: item - }); - }; - - onDeleteCredential = item => { - const heading = ( - - Are you sure you want to delete the credential {item.name}? - - ); - - const onConfirm = () => this.doDeleteCredentials([item]); - - store.dispatch({ - type: reduxTypes.confirmationModal.CONFIRMATION_MODAL_SHOW, - title: 'Delete Credential', - heading, - confirmButtonText: 'Delete', - onConfirm - }); - }; - - onAddSource = () => { - store.dispatch({ - type: reduxTypes.sources.CREATE_SOURCE_SHOW - }); - }; - - onRefresh = props => { - const { getCredentials, viewOptions } = this.props; - const options = _get(props, 'viewOptions') || viewOptions; - - getCredentials(helpers.createViewQueryObject(options)); - }; - - onClearFilters = () => { - store.dispatch({ - type: reduxTypes.viewToolbar.CLEAR_FILTERS, - viewType: reduxTypes.view.CREDENTIALS_VIEW - }); - }; - - deleteNextCredential() { - const { deleteCredential } = this.props; - - if (this.credentialsToDelete.length > 0) { - this.deletingCredential = this.credentialsToDelete.pop(); - if (this.deletingCredential) { - deleteCredential(this.deletingCredential.id); - } - } - } - - doDeleteCredentials(items) { - this.credentialsToDelete = [...items]; - - store.dispatch({ - type: reduxTypes.confirmationModal.CONFIRMATION_MODAL_HIDE - }); - - this.deleteNextCredential(); - } - - renderCredentialActions() { - const { t, viewOptions } = this.props; - +import { Table } from '../table/table'; +import { credentialsTableCells } from './credentialsTableCells'; +import { + useGetCredentials, + useOnDelete, + useOnEdit, + useOnExpand, + useOnRefresh, + useOnSelect +} from './credentialsContext'; +import { useOnShowAddSourceWizard } from '../addSourceWizard/addSourceWizardContext'; + +const VIEW_ID = 'credentials'; + +/** + * A credentials view. + * + * @param {object} props + * @param {Function} props.t + * @param {Function} props.useGetCredentials + * @param {Function} props.useOnDelete + * @param {Function} props.useOnEdit + * @param {Function} props.useOnExpand + * @param {Function} props.useOnRefresh + * @param {Function} props.useOnSelect + * @param {Function} props.useSelectors + * @param {Function} props.useOnShowAddSourceWizard + * @param {string} props.viewId + * @returns {React.ReactNode} + */ +const Credentials = ({ + t, + useGetCredentials: useAliasGetCredentials, + useOnDelete: useAliasOnDelete, + useOnEdit: useAliasOnEdit, + useOnExpand: useAliasOnExpand, + useOnRefresh: useAliasOnRefresh, + useOnSelect: useAliasOnSelect, + useSelectors: useAliasSelectors, + useOnShowAddSourceWizard: useAliasOnShowAddSourceWizard, + viewId +}) => { + const onExpand = useAliasOnExpand(); + const onRefresh = useAliasOnRefresh(); + const onDelete = useAliasOnDelete(); + const onEdit = useAliasOnEdit(); + const onSelect = useAliasOnSelect(); + const onShowAddSourceWizard = useAliasOnShowAddSourceWizard(); + const { pending, error, errorMessage, date, data, selectedRows = {}, expandedRows = {} } = useAliasGetCredentials(); + const [viewOptions = {}] = useAliasSelectors([ + ({ viewOptions: stateViewOptions }) => stateViewOptions[reduxTypes.view.CREDENTIALS_VIEW] + ]); + const isActive = viewOptions?.activeFilters?.length > 0 || data?.length > 0 || false; + + /** + * Toolbar actions onDeleteCredentials + * + * @event onDeleteCredentials + */ + const onDeleteCredentials = useCallback(() => { + const credentialsToDelete = Object.values(selectedRows).filter(val => val !== null); + onDelete(credentialsToDelete); + }, [onDelete, selectedRows]); + + /** + * Return toolbar actions. + * + * @returns {React.ReactNode} + */ + const renderToolbarActions = () => ( + + {' '} + + + ); + + if (pending) { return ( -
- {' '} - -
+ + +
{t('view.loading', { context: viewId })}
+
); } - renderPendingMessage() { - const { pending, t } = this.props; - - if (pending) { - return ( - -
-
{t('view.loading', { context: 'credentials' })}
- - ); - } - - return null; - } - - renderCredentialsList(items) { - const { t } = this.props; - - if (_size(items)) { - return ( - - {items.map(item => ( - - ))} - - ); - } - + if (error) { return ( - - - - {t('view.empty-state', { context: ['filter', 'title'] })} - - {t('view.empty-state', { context: ['filter', 'description'] })} - - - + + + {t('view.error-message', { context: [viewId], message: errorMessage })} + ); } - render() { - const { error, errorMessage, credentials, pending, t, viewOptions } = this.props; - const { lastRefresh } = this.state; - - if (pending) { - return this.renderPendingMessage(); - } - - if (error) { - return ( - - - {t('view.error-message', { context: ['credentials'], message: errorMessage })} - - - ); - } - - if (_size(credentials) || _size(viewOptions.activeFilters)) { - return ( + return ( +
+ {isActive && ( -
- - -
{this.renderCredentialsList(credentials)}
-
- {this.renderPendingMessage()} + onRefresh()} + lastRefresh={new Date(date).getTime()} + actions={renderToolbarActions()} + itemsType="Credential" + itemsTypePlural="Credentials" + selectedCount={viewOptions.selectedItems?.length} + {...viewOptions} + /> +
- ); - } - - return ( - - {this.renderPendingMessage()} - , - - ); - } -} + )} +
+ ({ + isSelected: (selectedRows?.[item.id] && true) || false, + item, + cells: [ + { + content: credentialsTableCells.description(item), + width: 35, + dataLabel: t('table.header', { context: ['description'] }) + }, + { + content: credentialsTableCells.authType(item, { viewId }), + dataLabel: t('table.header', { context: ['auth-type'] }) + }, + { + ...credentialsTableCells.sourcesCellContent(item, { viewId }), + isExpanded: expandedRows?.[item.id] === 2, + width: 8, + dataLabel: t('table.header', { context: ['sources'] }) + }, + { + style: { textAlign: 'right' }, + content: credentialsTableCells.actionsCell({ + isFirst: index === 0, + isLast: index === data.length - 1, + item, + onEdit: () => onEdit(item), + onDelete: () => onDelete(item) + }), + isActionCell: true + } + ] + }))} + > + +
+
+
+ ); +}; +/** + * Prop types + * + * @type {{useOnEdit: Function, useOnSelect: Function, viewId: string, t: Function, useOnRefresh: Function, + * useOnDelete: Function, useOnExpand: Function, useSelectors: Function, useGetCredentials: Function, + * useOnShowAddSourceWizard: Function}} + */ Credentials.propTypes = { - getCredentials: PropTypes.func, - deleteCredential: PropTypes.func, - fulfilled: PropTypes.bool, - error: PropTypes.bool, - errorMessage: PropTypes.string, - pending: PropTypes.bool, - credentials: PropTypes.array, - viewOptions: PropTypes.object, t: PropTypes.func, - update: PropTypes.object + useGetCredentials: PropTypes.func, + useOnDelete: PropTypes.func, + useOnEdit: PropTypes.func, + useOnExpand: PropTypes.func, + useOnRefresh: PropTypes.func, + useOnSelect: PropTypes.func, + useOnShowAddSourceWizard: PropTypes.func, + useSelectors: PropTypes.func, + viewId: PropTypes.string }; +/** + * Default props + * + * @type {{useOnEdit: Function, useOnSelect: Function, viewId: string, t: translate, useOnRefresh: Function, + * useOnDelete: Function, useOnExpand: Function, useSelectors: Function, useGetCredentials: Function, + * useOnShowAddSourceWizard: Function}} + */ Credentials.defaultProps = { - getCredentials: helpers.noop, - deleteCredential: helpers.noop, - fulfilled: false, - error: false, - errorMessage: null, - pending: false, - credentials: [], - viewOptions: {}, t: translate, - update: {} + useGetCredentials, + useOnDelete, + useOnEdit, + useOnExpand, + useOnRefresh, + useOnSelect, + useOnShowAddSourceWizard, + useSelectors: storeHooks.reactRedux.useSelectors, + viewId: VIEW_ID }; -const mapDispatchToProps = dispatch => ({ - getCredentials: queryObj => dispatch(reduxActions.credentials.getCredentials(queryObj)), - deleteCredential: id => dispatch(reduxActions.credentials.deleteCredential(id)) -}); - -const mapStateToProps = state => ({ - ...state.credentials.view, - viewOptions: state.viewOptions[reduxTypes.view.CREDENTIALS_VIEW] -}); - -const ConnectedCredentials = connect(mapStateToProps, mapDispatchToProps)(Credentials); - -export { ConnectedCredentials as default, ConnectedCredentials, Credentials }; +export { Credentials as default, Credentials, VIEW_ID }; diff --git a/src/components/credentials/credentialsContext.js b/src/components/credentials/credentialsContext.js new file mode 100644 index 00000000..d1a471be --- /dev/null +++ b/src/components/credentials/credentialsContext.js @@ -0,0 +1,295 @@ +import React, { useState } from 'react'; +import { useShallowCompareEffect } from 'react-use'; +import { AlertVariant, List, ListItem } from '@patternfly/react-core'; +import { ContextIcon, ContextIconVariant } from '../contextIcon/contextIcon'; +import { reduxActions, reduxTypes, storeHooks } from '../../redux'; +import { apiTypes } from '../../constants/apiConstants'; +import { helpers } from '../../common'; +import { translate } from '../i18n/i18n'; +import { useConfirmation } from '../../hooks/useConfirmation'; + +/** + * Credential action, onDelete. + * + * @param {object} options + * @param {Function} options.deleteCredentials + * @param {Function} options.t + * @param {Function} options.useConfirmation + * @param {Function} options.useDispatch + * @param {Function} options.useSelectorsResponse + * @returns {{onDelete: Function, onEdit: Function}} + */ +const useOnDelete = ({ + deleteCredentials = reduxActions.credentials.deleteCredential, + t = translate, + useConfirmation: useAliasConfirmation = useConfirmation, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse +} = {}) => { + const onConfirmation = useAliasConfirmation(); + const [credentialsToDelete, setCredentialsToDelete] = useState([]); + const dispatch = useAliasDispatch(); + const { + data, + error: deletedError, + fulfilled: deletedFulfilled + } = useAliasSelectorsResponse(({ credentials }) => credentials?.deleted); + const { errorMessage } = data?.[0] || {}; + + useShallowCompareEffect(() => { + if (credentialsToDelete.length) { + const credentialIds = credentialsToDelete.map(cred => cred[apiTypes.API_RESPONSE_CREDENTIAL_ID]); + deleteCredentials(credentialIds)(dispatch); + } + }, [credentialsToDelete, deleteCredentials, dispatch]); + + useShallowCompareEffect(() => { + if (deletedFulfilled && credentialsToDelete.length) { + const credentialNames = credentialsToDelete.map(cred => cred[apiTypes.API_RESPONSE_CREDENTIAL_NAME]); + dispatch([ + { + type: reduxTypes.toastNotifications.TOAST_ADD, + alertType: AlertVariant.success, + header: t('toast-notifications.title', { + context: ['deleted-credential'], + count: credentialNames.length + }), + message: t('toast-notifications.description', { + context: ['deleted-credential'], + name: credentialNames[0], + count: credentialNames.length + }) + }, + { + type: reduxTypes.credentials.DESELECT_CREDENTIAL, + item: credentialsToDelete + }, + { + type: reduxTypes.credentials.UPDATE_CREDENTIALS + } + ]); + + setCredentialsToDelete(() => []); + } + + if (deletedError && credentialsToDelete.length) { + const credentialNames = credentialsToDelete.map(cred => cred[apiTypes.API_RESPONSE_CREDENTIAL_NAME]); + dispatch([ + { + type: reduxTypes.toastNotifications.TOAST_ADD, + alertType: AlertVariant.danger, + header: t('toast-notifications.title', { + context: ['error'] + }), + message: t('toast-notifications.description', { + context: ['deleted-credential', 'error'], + name: credentialNames[0], + count: credentialNames.length, + message: errorMessage + }) + }, + { + type: reduxTypes.credentials.DESELECT_CREDENTIAL, + item: credentialsToDelete + }, + { + type: reduxTypes.credentials.UPDATE_CREDENTIALS + } + ]); + + setCredentialsToDelete(() => []); + } + }, [credentialsToDelete, deletedError, deletedFulfilled, dispatch, errorMessage, t]); + + /** + * Confirmation, and state, for deleting a single or multiple credentials + * + * @param {Array|string} credentials + */ + return credentials => { + const updatedCredentials = (Array.isArray(credentials) && credentials) || [credentials]; + + onConfirmation({ + title: t('form-dialog.confirmation', { + context: ['title', 'delete-credential'], + count: updatedCredentials.length + }), + heading: t( + 'form-dialog.confirmation', + { + context: ['heading', 'delete-credential'], + count: updatedCredentials.length, + name: updatedCredentials?.[0]?.[apiTypes.API_RESPONSE_CREDENTIAL_NAME] + }, + [] + ), + body: + (updatedCredentials.length > 1 && ( + + {updatedCredentials.map( + ({ + [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: name, + [apiTypes.API_RESPONSE_CREDENTIAL_CRED_TYPE]: credType + }) => ( + }> + {name} + + ) + )} + + )) || + undefined, + confirmButtonText: t('form-dialog.label', { + context: ['delete'] + }), + onConfirm: () => setCredentialsToDelete(() => updatedCredentials) + }); + }; +}; + +/** + * On edit a credential, show modal. + * + * @param {object} options + * @param {Function} options.useDispatch + * @returns {Function} + */ +const useOnEdit = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => { + const dispatch = useAliasDispatch(); + + return item => { + dispatch({ + type: reduxTypes.credentials.EDIT_CREDENTIAL_SHOW, + credential: item + }); + }; +}; + +/** + * On expand a row facet. + * + * @param {object} options + * @param {Function} options.useDispatch + * @returns {Function} + */ +const useOnExpand = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => { + const dispatch = useAliasDispatch(); + + return ({ isExpanded, cellIndex, data }) => { + dispatch({ + type: isExpanded ? reduxTypes.credentials.EXPANDED_CREDENTIAL : reduxTypes.credentials.NOT_EXPANDED_CREDENTIAL, + viewType: reduxTypes.view.CREDENTIALS_VIEW, + item: data.item, + cellIndex + }); + }; +}; + +/** + * On refresh view. + * + * @param {object} options + * @param {Function} options.useDispatch + * @returns {Function} + */ +const useOnRefresh = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => { + const dispatch = useAliasDispatch(); + + return () => { + dispatch({ + type: reduxTypes.credentials.UPDATE_CREDENTIALS + }); + }; +}; + +/** + * On select a row. + * + * @param {object} options + * @param {Function} options.useDispatch + * @returns {Function} + */ +const useOnSelect = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => { + const dispatch = useAliasDispatch(); + + return ({ isSelected, data }) => { + dispatch({ + type: isSelected ? reduxTypes.credentials.SELECT_CREDENTIAL : reduxTypes.credentials.DESELECT_CREDENTIAL, + viewType: reduxTypes.view.CREDENTIALS_VIEW, + item: data.item + }); + }; +}; + +/** + * Get credentials + * + * @param {object} options + * @param {Function} options.getCredentials + * @param {Function} options.useDispatch + * @param {Function} options.useSelectors + * @param {Function} options.useSelectorsResponse + * @returns {{date: *, data: *[], pending: boolean, errorMessage: null, fulfilled: boolean, selectedRows: *, + * expandedRows: *, error: boolean}} + */ +const useGetCredentials = ({ + getCredentials = reduxActions.credentials.getCredentials, + useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, + useSelectors: useAliasSelectors = storeHooks.reactRedux.useSelectors, + useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse +} = {}) => { + const dispatch = useAliasDispatch(); + const [refreshUpdate, selectedRows, expandedRows, viewOptions] = useAliasSelectors([ + ({ credentials }) => credentials?.update, + ({ credentials }) => credentials?.selected, + ({ credentials }) => credentials?.expanded, + ({ viewOptions: stateViewOptions }) => stateViewOptions?.[reduxTypes.view.CREDENTIALS_VIEW] + ]); + const { + data: responseData, + error, + fulfilled, + message: errorMessage, + pending, + responses = {} + } = useAliasSelectorsResponse({ id: 'view', selector: ({ credentials }) => credentials?.view }); + + const [{ date } = {}] = responses?.list || []; + const { results: data = [] } = responseData?.view || {}; + const query = helpers.createViewQueryObject(viewOptions); + + useShallowCompareEffect(() => { + getCredentials(null, query)(dispatch); + }, [dispatch, getCredentials, query, refreshUpdate]); + + return { + pending, + error, + errorMessage, + fulfilled, + data, + date, + selectedRows, + expandedRows + }; +}; + +const context = { + useGetCredentials, + useOnDelete, + useOnEdit, + useOnExpand, + useOnRefresh, + useOnSelect +}; + +export { + context as default, + context, + useGetCredentials, + useOnDelete, + useOnEdit, + useOnExpand, + useOnRefresh, + useOnSelect +}; diff --git a/src/components/credentials/credentialsEmptyState.js b/src/components/credentials/credentialsEmptyState.js index 57fba9b1..8a8b0be5 100644 --- a/src/components/credentials/credentialsEmptyState.js +++ b/src/components/credentials/credentialsEmptyState.js @@ -24,21 +24,22 @@ import { translate } from '../i18n/i18n'; * @param {Function} props.t * @param {string} props.uiSentenceStartName * @param {string} props.uiShortName + * @param {string} props.viewId * @returns {React.ReactNode} */ -const CredentialsEmptyState = ({ onAddSource, t, uiSentenceStartName, uiShortName }) => ( +const CredentialsEmptyState = ({ onAddSource, t, uiSentenceStartName, uiShortName, viewId }) => ( {t('view.empty-state', { context: 'title', name: uiShortName })} - {t('view.empty-state', { context: ['description', 'credentials'], name: uiSentenceStartName })} + {t('view.empty-state', { context: ['description', viewId], name: uiSentenceStartName })} @@ -53,7 +54,8 @@ CredentialsEmptyState.propTypes = { onAddSource: PropTypes.func, t: PropTypes.func, uiSentenceStartName: PropTypes.string, - uiShortName: PropTypes.string + uiShortName: PropTypes.string, + viewId: PropTypes.string }; /** @@ -65,7 +67,8 @@ CredentialsEmptyState.defaultProps = { onAddSource: helpers.noop, t: translate, uiSentenceStartName: helpers.UI_SENTENCE_START_NAME, - uiShortName: helpers.UI_SHORT_NAME + uiShortName: helpers.UI_SHORT_NAME, + viewId: null }; export { CredentialsEmptyState as default, CredentialsEmptyState }; diff --git a/src/components/credentials/credentialsTableCells.js b/src/components/credentials/credentialsTableCells.js new file mode 100644 index 00000000..1032b981 --- /dev/null +++ b/src/components/credentials/credentialsTableCells.js @@ -0,0 +1,237 @@ +import React from 'react'; +import { + Button, + ButtonVariant, + Grid, + GridItem, + OverflowMenu, + OverflowMenuControl, + OverflowMenuContent, + OverflowMenuGroup, + OverflowMenuItem, + List, + ListItem +} from '@patternfly/react-core'; +import { EllipsisVIcon, EyeIcon, TrashIcon } from '@patternfly/react-icons'; +import { ContextIcon, ContextIconVariant } from '../contextIcon/contextIcon'; +import { Tooltip } from '../tooltip/tooltip'; +import { apiTypes } from '../../constants/apiConstants'; +import { translate } from '../i18n/i18n'; +import { helpers } from '../../common'; +import { DropdownSelect, SelectButtonVariant, SelectDirection, SelectPosition } from '../dropdownSelect/dropdownSelect'; + +/** + * Source description and type icon + * + * @param {object} params + * @param {object} options + * @param {Function} options.t + * @param {string} options.viewId + * @returns {React.ReactNode} + */ +const description = ( + { + [apiTypes.API_RESPONSE_CREDENTIAL_ID]: id, + [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: name, + [apiTypes.API_RESPONSE_CREDENTIAL_CRED_TYPE]: credType + } = {}, + { t = translate, viewId } = {} +) => ( + + + + + + + +
+ {name || id} +
+
+
+); + +/** + * Scan status, icon and description + * + * @param {object} params + * @param {object} options + * @param {Function} options.t + * @param {string} options.viewId + * @returns {React.ReactNode|null} + */ +const authType = ( + { [apiTypes.API_RESPONSE_CREDENTIAL_SSH_KEYFILE]: sshKeyfile } = {}, + { t = translate, viewId } = {} +) => ( + + {t('table.label', { context: ['auth', 'cell', sshKeyfile !== '' && 'sshKey', viewId] })} + +); + +/** + * Generate a consistent status cell. + * + * @param {object} params + * @param {number} params.count + * @param {string} params.status + * @param {Function} params.t + * @param {string} params.viewId + * @returns {React.ReactNode} + */ +const statusCell = ({ count, status = ContextIconVariant.unknown, t = translate, viewId } = {}) => { + let updatedCount = count || 0; + + if (helpers.DEV_MODE) { + updatedCount = helpers.devModeNormalizeCount(updatedCount); + } + + return ( + + {t('table.label', { context: ['status', 'cell', viewId], count: updatedCount }, [ + , + + ])} + + ); +}; + +/** + * Generate sources expandable content. + * + * @param {object} source + * @returns {React.ReactNode} + */ +const sourcesContent = ({ [apiTypes.API_RESPONSE_CREDENTIAL_SOURCES]: sources } = {}) => { + const updatedSources = (sources && [...sources]) || []; + + updatedSources.sort((item1, item2) => + item1[apiTypes.API_RESPONSE_CREDENTIAL_SOURCES_NAME].localeCompare( + item2[apiTypes.API_RESPONSE_CREDENTIAL_SOURCES_NAME] + ) + ); + + return ( + + {updatedSources?.map(source => ( + + } + > + {source[apiTypes.API_RESPONSE_CREDENTIAL_SOURCES_NAME]} + + ))} + + ); +}; + +const sourcesCellContent = (item = {}, { viewId } = {}) => { + const { [apiTypes.API_RESPONSE_CREDENTIAL_SOURCES]: sources } = item; + const count = sources?.length; + + return { + content: statusCell({ count, status: 'sources', viewId }), + expandedContent: (count && sourcesContent(item)) || undefined + }; +}; + +// FixMe: PF Overflow menu is attempting state updates on unmounted components +/** + * Action cell content + * + * @param {object} params + * @param {boolean} params.isFirst + * @param {boolean} params.isLast + * @param {object} params.item + * @param {Function} params.onDelete + * @param {Function} params.onEdit + * @param {Function} params.t + * @returns {React.ReactNode} + */ +const actionsCell = ({ + isFirst = false, + isLast = false, + item = {}, + onDelete = helpers.noop, + onEdit = helpers.noop, + t = translate +} = {}) => { + const onSelect = ({ value }) => { + switch (value) { + case 'delete': + return onDelete(item); + case 'edit': + default: + return onEdit(item); + } + }; + + return ( + + + + + + + + + + + + + + + + + } + options={[ + { title: t('table.label', { context: 'edit' }), value: 'edit' }, + { title: t('table.label', { context: 'delete' }), value: 'delete' } + ]} + /> + + + ); +}; + +const credentialsTableCells = { + actionsCell, + authType, + description, + sourcesCellContent, + sourcesContent, + statusCell +}; + +export { + credentialsTableCells as default, + credentialsTableCells, + actionsCell, + authType, + description, + sourcesCellContent, + sourcesContent, + statusCell +}; diff --git a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap index 4249d282..367fb7b9 100644 --- a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap +++ b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap @@ -212,15 +212,19 @@ Array [ "keys": Array [ Object { "key": "form-dialog.label", - "match": "t('form-dialog.label', { context: ['submit', 'confirmation'] })", + "match": "t('form-dialog.label', { context: ['confirm'] })", }, Object { "key": "form-dialog.label", - "match": "t('form-dialog.label', { context: 'cancel' })", + "match": "t('form-dialog.label', { context: ['cancel'] })", }, Object { - "key": "form-dialog.label", - "match": "t('form-dialog.label', { context: ['submit', 'confirmation'] })", + "key": "form-dialog.confirmation", + "match": "t('form-dialog.confirmation', { context: ['heading'] })", + }, + Object { + "key": "form-dialog.confirmation", + "match": "t('form-dialog.confirmation', { context: ['title'] })", }, ], }, @@ -267,27 +271,60 @@ Array [ }, Object { "key": "view.loading", - "match": "t('view.loading', { context: 'credentials' })", + "match": "t('view.loading', { context: viewId })", }, Object { - "key": "view.empty-state", - "match": "t('view.empty-state', { context: ['filter', 'title'] })", + "key": "view.error", + "match": "t('view.error', { context: viewId })", }, Object { - "key": "view.empty-state", - "match": "t('view.empty-state', { context: ['filter', 'description'] })", + "key": "view.error-message", + "match": "t('view.error-message', { context: [viewId], message: errorMessage })", }, Object { - "key": "view.empty-state", - "match": "t('view.empty-state', { context: ['label', 'clear'] })", + "key": "table.header", + "match": "t('table.header', { context: ['description'] })", }, Object { - "key": "view.error", - "match": "t('view.error', { context: 'credentials' })", + "key": "table.header", + "match": "t('table.header', { context: ['auth-type'] })", }, Object { - "key": "view.error-message", - "match": "t('view.error-message', { context: ['credentials'], message: errorMessage })", + "key": "table.header", + "match": "t('table.header', { context: ['sources'] })", + }, + ], + }, + Object { + "file": "./src/components/credentials/credentialsContext.js", + "keys": Array [ + Object { + "key": "toast-notifications.title", + "match": "t('toast-notifications.title', { context: ['deleted-credential'], count: credentialNames.length })", + }, + Object { + "key": "toast-notifications.description", + "match": "t('toast-notifications.description', { context: ['deleted-credential'], name: credentialNames[0], count: credentialNames.length })", + }, + Object { + "key": "toast-notifications.title", + "match": "t('toast-notifications.title', { context: ['error'] })", + }, + Object { + "key": "toast-notifications.description", + "match": "t('toast-notifications.description', { context: ['deleted-credential', 'error'], name: credentialNames[0], count: credentialNames.length, message: errorMessage })", + }, + Object { + "key": "form-dialog.confirmation", + "match": "t('form-dialog.confirmation', { context: ['title', 'delete-credential'], count: updatedCredentials.length })", + }, + Object { + "key": "form-dialog.confirmation", + "match": "t( 'form-dialog.confirmation', { context: ['heading', 'delete-credential'], count: updatedCredentials.length, name: updatedCredentials?.[0]?.[apiTypes.API_RESPONSE_CREDENTIAL_NAME] }, [] )", + }, + Object { + "key": "form-dialog.label", + "match": "t('form-dialog.label', { context: ['delete'] })", }, ], }, @@ -300,11 +337,60 @@ Array [ }, Object { "key": "view.empty-state", - "match": "t('view.empty-state', { context: ['description', 'credentials'], name: uiSentenceStartName })", + "match": "t('view.empty-state', { context: ['description', viewId], name: uiSentenceStartName })", }, Object { "key": "view.empty-state", - "match": "t('view.empty-state', { context: ['label', 'source'] })", + "match": "t('view.empty-state', { context: ['label', 'sources'] })", + }, + ], + }, + Object { + "file": "./src/components/credentials/credentialsTableCells.js", + "keys": Array [ + Object { + "key": "table.label", + "match": "t('table.label', { context: [credType, viewId] })", + }, + Object { + "key": "table.label", + "match": "t('table.label', { context: ['auth', 'tooltip', viewId] })", + }, + Object { + "key": "table.label", + "match": "t('table.label', { context: ['auth', 'cell', sshKeyfile !== '' && 'sshKey', viewId] })", + }, + Object { + "key": "table.label", + "match": "t('table.label', { context: ['status', 'tooltip', status, viewId], count: updatedCount })", + }, + Object { + "key": "table.label", + "match": "t('table.label', { context: ['status', 'cell', viewId], count: updatedCount }, [ , ])", + }, + Object { + "key": "table.label", + "match": "t('table.label', { context: 'view' })", + }, + Object { + "key": "table.label", + "match": "t('table.label', { context: 'view' })", + }, + Object { + "key": "table.label", + "match": "t('table.label', { context: 'delete' })", + }, + Object { + "key": "table.label", + "match": "t('table.label', { context: 'delete' })", + }, + Object { + "key": "table.label", + "match": "t('table.label', { context: 'edit' })", + }, + Object { + "key": "table.label", + "match": "t('table.label', { context: 'delete' })", }, ], }, @@ -616,6 +702,10 @@ Array [ "key": "table.label", "match": "t('table.label', { context: 'network-range' })", }, + Object { + "key": "table.label", + "match": "t('table.label', { context: [sourceType, viewId] })", + }, Object { "key": "table.label", "match": "t('table.label', { context: ['status', status, viewId] })", @@ -716,25 +806,41 @@ Array [ "file": "./src/components/authentication/authentication.js", "key": "view.error-message", }, + Object { + "file": "./src/components/confirmationModal/confirmationModal.js", + "key": "form-dialog.confirmation", + }, + Object { + "file": "./src/components/confirmationModal/confirmationModal.js", + "key": "form-dialog.confirmation", + }, Object { "file": "./src/components/credentials/credentials.js", - "key": "view.empty-state", + "key": "view.error", }, Object { "file": "./src/components/credentials/credentials.js", - "key": "view.empty-state", + "key": "view.error-message", }, Object { "file": "./src/components/credentials/credentials.js", - "key": "view.empty-state", + "key": "table.header", }, Object { "file": "./src/components/credentials/credentials.js", - "key": "view.error", + "key": "table.header", }, Object { "file": "./src/components/credentials/credentials.js", - "key": "view.error-message", + "key": "table.header", + }, + Object { + "file": "./src/components/credentials/credentialsContext.js", + "key": "form-dialog.confirmation", + }, + Object { + "file": "./src/components/credentials/credentialsContext.js", + "key": "form-dialog.confirmation", }, Object { "file": "./src/components/credentials/credentialsEmptyState.js", diff --git a/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap b/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap index 0c3f060c..3cd3340c 100644 --- a/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap +++ b/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap @@ -376,12 +376,7 @@ exports[`PageLayout Component should render a non-connected component unauthoriz "to": "/scans", }, Object { - "component": Object { - "$$typeof": Symbol(react.memo), - "WrappedComponent": [Function], - "compare": null, - "type": [Function], - }, + "component": [Function], "iconClass": "fa fa-id-card", "title": "Credentials", "to": "/credentials", diff --git a/src/components/router/__tests__/__snapshots__/router.test.js.snap b/src/components/router/__tests__/__snapshots__/router.test.js.snap index 131c11b0..126d0ac8 100644 --- a/src/components/router/__tests__/__snapshots__/router.test.js.snap +++ b/src/components/router/__tests__/__snapshots__/router.test.js.snap @@ -16,14 +16,7 @@ exports[`Router Component should shallow render a basic component 1`] = ` path="/scans" /> diff --git a/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap b/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap index 06913230..d0c47e78 100644 --- a/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap +++ b/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap @@ -18,12 +18,7 @@ Array [ "to": "/scans", }, Object { - "component": Object { - "$$typeof": Symbol(react.memo), - "WrappedComponent": [Function], - "compare": null, - "type": [Function], - }, + "component": [Function], "iconClass": "fa fa-id-card", "title": "Credentials", "to": "/credentials", diff --git a/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap b/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap index e4d48761..f4629dfc 100644 --- a/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap +++ b/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ScansContext should apply a hook for retreiving data from multiple selectors: responses 1`] = ` +exports[`ScansContext should apply a hook for retrieving data from multiple selectors: responses 1`] = ` Object { "errorResponse": Object { "data": Array [], diff --git a/src/components/scans/__tests__/scansContext.test.js b/src/components/scans/__tests__/scansContext.test.js index 39b56fcc..5862f429 100644 --- a/src/components/scans/__tests__/scansContext.test.js +++ b/src/components/scans/__tests__/scansContext.test.js @@ -55,7 +55,7 @@ describe('ScansContext', () => { expect(mockUseTimeout.mock.calls).toMatchSnapshot('timeout'); }); - it('should apply a hook for retreiving data from multiple selectors', () => { + it('should apply a hook for retrieving data from multiple selectors', () => { const { result: errorResponse } = shallowHook(() => useGetScans({ useSelectorsResponse: () => ({ error: true, message: 'Lorem ipsum' }) diff --git a/src/components/sources/__tests__/__snapshots__/sources.test.js.snap b/src/components/sources/__tests__/__snapshots__/sources.test.js.snap index 3329655b..27e62b21 100644 --- a/src/components/sources/__tests__/__snapshots__/sources.test.js.snap +++ b/src/components/sources/__tests__/__snapshots__/sources.test.js.snap @@ -151,7 +151,7 @@ exports[`Sources Component should handle multiple display states, pending, error sm={2} > { expect(mockUseTimeout.mock.calls).toMatchSnapshot('timeout'); }); - it('should apply a hook for retreiving data from multiple selectors', () => { + it('should apply a hook for retrieving data from multiple selectors', () => { const { result: errorResponse } = shallowHook(() => useGetSources({ useSelectorsResponse: () => ({ error: true, message: 'Lorem ipsum' }) diff --git a/src/components/sources/sourcesTableCells.js b/src/components/sources/sourcesTableCells.js index 25d7298f..965dbabe 100644 --- a/src/components/sources/sourcesTableCells.js +++ b/src/components/sources/sourcesTableCells.js @@ -15,7 +15,6 @@ import { import { PencilAltIcon, TrashIcon, EllipsisVIcon } from '@patternfly/react-icons'; import { ContextIcon, ContextIconVariant } from '../contextIcon/contextIcon'; import { Tooltip } from '../tooltip/tooltip'; -import { dictionary } from '../../constants/dictionaryConstants'; import { ConnectedScanHostList as ScanHostList } from '../scanHostList/scanHostList'; import { apiTypes } from '../../constants/apiConstants'; import { translate } from '../i18n/i18n'; @@ -31,9 +30,10 @@ import { DropdownSelect, SelectButtonVariant, SelectDirection, SelectPosition } * @param {string} params.source_type * @param {object} options * @param {Function} options.t + * @param {string} options.viewId * @returns {React.ReactNode} */ -const description = ({ hosts, name, source_type: sourceType } = {}, { t = translate } = {}) => { +const description = ({ hosts, name, source_type: sourceType } = {}, { t = translate, viewId } = {}) => { const itemHostsPopover = (
{hosts?.length > 1 && ( @@ -66,7 +66,7 @@ const description = ({ hosts, name, source_type: sourceType } = {}, { t = transl return ( - + diff --git a/src/components/table/__tests__/__snapshots__/tableHelpers.test.js.snap b/src/components/table/__tests__/__snapshots__/tableHelpers.test.js.snap index eb7718a4..7ffb18cb 100644 --- a/src/components/table/__tests__/__snapshots__/tableHelpers.test.js.snap +++ b/src/components/table/__tests__/__snapshots__/tableHelpers.test.js.snap @@ -577,7 +577,7 @@ Object { , "key": "W29iamVjdCBPYmplY3Rd-3-0", "props": Object { - "className": "", + "className": " pf-m-width-11", "dataLabel": undefined, "isActionCell": undefined, "noPadding": undefined, diff --git a/src/constants/__tests__/__snapshots__/apiConstants.test.js.snap b/src/constants/__tests__/__snapshots__/apiConstants.test.js.snap index 5f49ffff..8fd5b18c 100644 --- a/src/constants/__tests__/__snapshots__/apiConstants.test.js.snap +++ b/src/constants/__tests__/__snapshots__/apiConstants.test.js.snap @@ -13,6 +13,10 @@ Object { "API_RESPONSE_CREDENTIAL_CRED_TYPE": "cred_type", "API_RESPONSE_CREDENTIAL_ID": "id", "API_RESPONSE_CREDENTIAL_NAME": "name", + "API_RESPONSE_CREDENTIAL_SOURCES": "sources", + "API_RESPONSE_CREDENTIAL_SOURCES_NAME": "name", + "API_RESPONSE_CREDENTIAL_SOURCES_SOURCE_TYPE": "source_type", + "API_RESPONSE_CREDENTIAL_SSH_KEYFILE": "ssh_keyfile", "API_RESPONSE_JOBS_COUNT": "count", "API_RESPONSE_JOBS_NEXT": "next", "API_RESPONSE_JOBS_RESULTS": "results", @@ -118,6 +122,10 @@ Object { "API_RESPONSE_CREDENTIAL_CRED_TYPE": "cred_type", "API_RESPONSE_CREDENTIAL_ID": "id", "API_RESPONSE_CREDENTIAL_NAME": "name", + "API_RESPONSE_CREDENTIAL_SOURCES": "sources", + "API_RESPONSE_CREDENTIAL_SOURCES_NAME": "name", + "API_RESPONSE_CREDENTIAL_SOURCES_SOURCE_TYPE": "source_type", + "API_RESPONSE_CREDENTIAL_SSH_KEYFILE": "ssh_keyfile", "API_RESPONSE_JOBS_COUNT": "count", "API_RESPONSE_JOBS_NEXT": "next", "API_RESPONSE_JOBS_RESULTS": "results", @@ -224,6 +232,10 @@ Object { "API_RESPONSE_CREDENTIAL_CRED_TYPE": "cred_type", "API_RESPONSE_CREDENTIAL_ID": "id", "API_RESPONSE_CREDENTIAL_NAME": "name", + "API_RESPONSE_CREDENTIAL_SOURCES": "sources", + "API_RESPONSE_CREDENTIAL_SOURCES_NAME": "name", + "API_RESPONSE_CREDENTIAL_SOURCES_SOURCE_TYPE": "source_type", + "API_RESPONSE_CREDENTIAL_SSH_KEYFILE": "ssh_keyfile", "API_RESPONSE_JOBS_COUNT": "count", "API_RESPONSE_JOBS_NEXT": "next", "API_RESPONSE_JOBS_RESULTS": "results", @@ -334,6 +346,10 @@ Object { "API_RESPONSE_CREDENTIAL_CRED_TYPE": "cred_type", "API_RESPONSE_CREDENTIAL_ID": "id", "API_RESPONSE_CREDENTIAL_NAME": "name", + "API_RESPONSE_CREDENTIAL_SOURCES": "sources", + "API_RESPONSE_CREDENTIAL_SOURCES_NAME": "name", + "API_RESPONSE_CREDENTIAL_SOURCES_SOURCE_TYPE": "source_type", + "API_RESPONSE_CREDENTIAL_SSH_KEYFILE": "ssh_keyfile", "API_RESPONSE_JOBS_COUNT": "count", "API_RESPONSE_JOBS_NEXT": "next", "API_RESPONSE_JOBS_RESULTS": "results", diff --git a/src/constants/apiConstants.js b/src/constants/apiConstants.js index d98bfecb..17518468 100644 --- a/src/constants/apiConstants.js +++ b/src/constants/apiConstants.js @@ -1,6 +1,10 @@ const API_RESPONSE_CREDENTIAL_CRED_TYPE = 'cred_type'; const API_RESPONSE_CREDENTIAL_NAME = 'name'; const API_RESPONSE_CREDENTIAL_ID = 'id'; +const API_RESPONSE_CREDENTIAL_SOURCES = 'sources'; +const API_RESPONSE_CREDENTIAL_SOURCES_NAME = 'name'; +const API_RESPONSE_CREDENTIAL_SOURCES_SOURCE_TYPE = 'source_type'; +const API_RESPONSE_CREDENTIAL_SSH_KEYFILE = 'ssh_keyfile'; const API_RESPONSE_CREDENTIALS_COUNT = 'count'; const API_RESPONSE_CREDENTIALS_RESULTS = 'results'; @@ -122,6 +126,10 @@ const apiTypes = { API_RESPONSE_CREDENTIAL_CRED_TYPE, API_RESPONSE_CREDENTIAL_NAME, API_RESPONSE_CREDENTIAL_ID, + API_RESPONSE_CREDENTIAL_SOURCES, + API_RESPONSE_CREDENTIAL_SSH_KEYFILE, + API_RESPONSE_CREDENTIAL_SOURCES_NAME, + API_RESPONSE_CREDENTIAL_SOURCES_SOURCE_TYPE, API_RESPONSE_CREDENTIALS_COUNT, API_RESPONSE_CREDENTIALS_RESULTS, API_RESPONSE_SCAN_ID, @@ -231,6 +239,10 @@ export { API_RESPONSE_CREDENTIAL_CRED_TYPE, API_RESPONSE_CREDENTIAL_NAME, API_RESPONSE_CREDENTIAL_ID, + API_RESPONSE_CREDENTIAL_SOURCES, + API_RESPONSE_CREDENTIAL_SSH_KEYFILE, + API_RESPONSE_CREDENTIAL_SOURCES_NAME, + API_RESPONSE_CREDENTIAL_SOURCES_SOURCE_TYPE, API_RESPONSE_CREDENTIALS_COUNT, API_RESPONSE_CREDENTIALS_RESULTS, API_RESPONSE_SCAN_ID, diff --git a/src/hooks/__tests__/__snapshots__/useConfirmation.test.js.snap b/src/hooks/__tests__/__snapshots__/useConfirmation.test.js.snap new file mode 100644 index 00000000..ffe3301a --- /dev/null +++ b/src/hooks/__tests__/__snapshots__/useConfirmation.test.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`useConfirmation should apply a hook for useConfirmation: dispatch confirmation 1`] = ` +Array [ + Array [ + Object { + "cancelButtonText": undefined, + "confirmButtonText": undefined, + "heading": "Lorem ipsum", + "onConfirm": [Function], + "title": "Dolor sit", + "type": "CONFIRMATION_MODAL_SHOW", + }, + ], +] +`; diff --git a/src/hooks/__tests__/useConfirmation.test.js b/src/hooks/__tests__/useConfirmation.test.js new file mode 100644 index 00000000..4ae76993 --- /dev/null +++ b/src/hooks/__tests__/useConfirmation.test.js @@ -0,0 +1,13 @@ +import { useConfirmation } from '../useConfirmation'; + +describe('useConfirmation', () => { + it('should apply a hook for useConfirmation', async () => { + const mockDispatch = jest.fn(); + const { result } = await mountHook(() => useConfirmation({ useDispatch: () => mockDispatch })); + + result({ heading: 'Lorem ipsum', title: 'Dolor sit' }); + + expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch confirmation'); + mockDispatch.mockClear(); + }); +}); diff --git a/src/hooks/useConfirmation.js b/src/hooks/useConfirmation.js new file mode 100644 index 00000000..fd9ca78e --- /dev/null +++ b/src/hooks/useConfirmation.js @@ -0,0 +1,48 @@ +import { reduxTypes, storeHooks } from '../redux'; +import { helpers } from '../common'; + +/** + * Base confirmation behavior + * + * @param {object} options + * @param {Function} options.useDispatch + * @returns {(function({confirmButtonText: *, heading: *, title: *}=): void)|*} + */ +const useConfirmation = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => { + const dispatch = useAliasDispatch(); + + return ({ + cancelButtonText, + confirmButtonText, + heading, + onConfirm = helpers.noop, + title, + ...confirmationOptions + } = {}) => { + let updatedOnConfirm; + + if (typeof onConfirm === 'function') { + updatedOnConfirm = () => { + dispatch([ + { + type: reduxTypes.confirmationModal.CONFIRMATION_MODAL_HIDE + } + ]); + + onConfirm(); + }; + } + + dispatch({ + ...confirmationOptions, + type: reduxTypes.confirmationModal.CONFIRMATION_MODAL_SHOW, + title, + heading, + confirmButtonText, + cancelButtonText, + onConfirm: updatedOnConfirm + }); + }; +}; + +export { useConfirmation as default, useConfirmation }; diff --git a/src/redux/actions/__tests__/__snapshots__/credentialsActions.test.js.snap b/src/redux/actions/__tests__/__snapshots__/credentialsActions.test.js.snap deleted file mode 100644 index d3ac004a..00000000 --- a/src/redux/actions/__tests__/__snapshots__/credentialsActions.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CredentialsActions Should return response content for getCredential method: getCredential 1`] = `"GET_CREDENTIAL_FULFILLED"`; diff --git a/src/redux/actions/__tests__/credentialsActions.test.js b/src/redux/actions/__tests__/credentialsActions.test.js index b507ee4e..af142d97 100644 --- a/src/redux/actions/__tests__/credentialsActions.test.js +++ b/src/redux/actions/__tests__/credentialsActions.test.js @@ -22,6 +22,8 @@ describe('CredentialsActions', () => { const request = moxios.requests.mostRecent(); request.respondWith({ status: 200, + responseText: 'success', + timeout: 1, response: { test: 'success', [apiTypes.API_RESPONSE_CREDENTIALS_RESULTS]: ['success'] @@ -34,24 +36,13 @@ describe('CredentialsActions', () => { moxios.uninstall(); }); - it('Should return response content for addAccount method', done => { + it('Should return response content for addCredential method', done => { const store = generateStore(); const dispatcher = credentialsActions.addCredential(); dispatcher(store.dispatch).then(() => { const response = store.getState().credentials; - - expect(response.update.credential.test).toEqual('success'); - done(); - }); - }); - - it('Should return response content for getCredential method', done => { - const store = generateStore(); - const dispatcher = credentialsActions.getCredential(); - - dispatcher(store.dispatch).then(value => { - expect(value.action.type).toMatchSnapshot('getCredential'); + expect(response.dialog.fulfilled).toEqual(true); done(); }); }); @@ -62,8 +53,7 @@ describe('CredentialsActions', () => { dispatcher(store.dispatch).then(() => { const response = store.getState().credentials; - - expect(response.view.credentials[0]).toEqual('success'); + expect(response.view.fulfilled).toEqual(true); done(); }); }); @@ -74,32 +64,18 @@ describe('CredentialsActions', () => { dispatcher(store.dispatch).then(() => { const response = store.getState().credentials; - - expect(response.update.credential.test).toEqual('success'); + expect(response.dialog.fulfilled).toEqual(true); done(); }); }); it('Should return response content for deleteCredential method', done => { const store = generateStore(); - const dispatcher = credentialsActions.deleteCredential(); + const dispatcher = credentialsActions.deleteCredential(['loremId']); dispatcher(store.dispatch).then(() => { const response = store.getState().credentials; - - expect(response.update.delete).toEqual(true); - done(); - }); - }); - - it('Should return response content for deleteCredentials method', done => { - const store = generateStore(); - const dispatcher = credentialsActions.deleteCredentials(); - - dispatcher(store.dispatch).then(() => { - const response = store.getState().credentials; - - expect(response.update.delete).toEqual(true); + expect(response.deleted.fulfilled).toEqual(true); done(); }); }); diff --git a/src/redux/actions/credentialsActions.js b/src/redux/actions/credentialsActions.js index c8bc076b..3d0f1e90 100644 --- a/src/redux/actions/credentialsActions.js +++ b/src/redux/actions/credentialsActions.js @@ -7,18 +7,12 @@ const addCredential = data => dispatch => payload: credentialsService.addCredential(data) }); -const getCredential = id => dispatch => - dispatch({ - type: credentialsTypes.GET_CREDENTIAL, - payload: credentialsService.getCredential(id) - }); - const getCredentials = - (query = {}) => + (id, query = {}) => dispatch => dispatch({ type: credentialsTypes.GET_CREDENTIALS, - payload: credentialsService.getCredentials('', query) + payload: credentialsService.getCredentials(id || '', query) }); const updateCredential = (id, data) => dispatch => @@ -27,25 +21,18 @@ const updateCredential = (id, data) => dispatch => payload: credentialsService.updateCredential(id, data) }); -const deleteCredential = id => dispatch => - dispatch({ +const deleteCredential = id => dispatch => { + const updatedIds = (Array.isArray(id) && id) || [id]; + + return dispatch({ type: credentialsTypes.DELETE_CREDENTIAL, - payload: credentialsService.deleteCredential(id) + payload: Promise.all(updatedIds.map(updatedId => credentialsService.deleteCredential(updatedId))) }); - -const deleteCredentials = - (ids = []) => - dispatch => - dispatch({ - type: credentialsTypes.DELETE_CREDENTIALS, - payload: credentialsService.deleteCredentials(ids) - }); +}; const credentialsActions = { addCredential, deleteCredential, - deleteCredentials, - getCredential, getCredentials, updateCredential }; @@ -55,8 +42,6 @@ export { credentialsActions, addCredential, deleteCredential, - deleteCredentials, - getCredential, getCredentials, updateCredential }; diff --git a/src/redux/constants/__tests__/__snapshots__/index.test.js.snap b/src/redux/constants/__tests__/__snapshots__/index.test.js.snap index fb874b78..c39d72e2 100644 --- a/src/redux/constants/__tests__/__snapshots__/index.test.js.snap +++ b/src/redux/constants/__tests__/__snapshots__/index.test.js.snap @@ -12,15 +12,22 @@ Object { }, "credentialsTypes": Object { "ADD_CREDENTIAL": "ADD_CREDENTIAL", + "CONFIRM_DELETE_CREDENTIAL": "CONFIRM_DELETE_CREDENTIAL", "CREATE_CREDENTIAL_SHOW": "CREATE_CREDENTIAL_SHOW", "DELETE_CREDENTIAL": "DELETE_CREDENTIAL", "DELETE_CREDENTIALS": "DELETE_CREDENTIALS", + "DESELECT_CREDENTIAL": "DESELECT_CREDENTIAL", "EDIT_CREDENTIAL_SHOW": "EDIT_CREDENTIAL_SHOW", + "EXPANDED_CREDENTIAL": "EXPANDED_CREDENTIAL", "GET_CREDENTIAL": "GET_CREDENTIAL", "GET_CREDENTIALS": "GET_CREDENTIALS", "GET_WIZARD_CREDENTIALS": "GET_WIZARD_CREDENTIALS", + "NOT_EXPANDED_CREDENTIAL": "NOT_EXPANDED_CREDENTIAL", "RESET_CREDENTIAL_UPDATE_STATUS": "RESET_CREDENTIAL_UPDATE_STATUS", + "RESET_DELETE_CREDENTIAL": "RESET_DELETE_CREDENTIAL", + "SELECT_CREDENTIAL": "SELECT_CREDENTIAL", "UPDATE_CREDENTIAL": "UPDATE_CREDENTIAL", + "UPDATE_CREDENTIALS": "UPDATE_CREDENTIALS", "UPDATE_CREDENTIAL_HIDE": "UPDATE_CREDENTIAL_HIDE", }, "default": Object { @@ -34,15 +41,22 @@ Object { }, "credentials": Object { "ADD_CREDENTIAL": "ADD_CREDENTIAL", + "CONFIRM_DELETE_CREDENTIAL": "CONFIRM_DELETE_CREDENTIAL", "CREATE_CREDENTIAL_SHOW": "CREATE_CREDENTIAL_SHOW", "DELETE_CREDENTIAL": "DELETE_CREDENTIAL", "DELETE_CREDENTIALS": "DELETE_CREDENTIALS", + "DESELECT_CREDENTIAL": "DESELECT_CREDENTIAL", "EDIT_CREDENTIAL_SHOW": "EDIT_CREDENTIAL_SHOW", + "EXPANDED_CREDENTIAL": "EXPANDED_CREDENTIAL", "GET_CREDENTIAL": "GET_CREDENTIAL", "GET_CREDENTIALS": "GET_CREDENTIALS", "GET_WIZARD_CREDENTIALS": "GET_WIZARD_CREDENTIALS", + "NOT_EXPANDED_CREDENTIAL": "NOT_EXPANDED_CREDENTIAL", "RESET_CREDENTIAL_UPDATE_STATUS": "RESET_CREDENTIAL_UPDATE_STATUS", + "RESET_DELETE_CREDENTIAL": "RESET_DELETE_CREDENTIAL", + "SELECT_CREDENTIAL": "SELECT_CREDENTIAL", "UPDATE_CREDENTIAL": "UPDATE_CREDENTIAL", + "UPDATE_CREDENTIALS": "UPDATE_CREDENTIALS", "UPDATE_CREDENTIAL_HIDE": "UPDATE_CREDENTIAL_HIDE", }, "facts": Object { @@ -75,52 +89,6 @@ Object { "SELECT_SCAN": "SELECT_SCAN", "START_SCAN": "START_SCAN", "UPDATE_SCANS": "UPDATE_SCANS", - "default": Object { - "ADD_SCAN": "ADD_SCAN", - "ADD_START_SCAN": "ADD_START_SCAN", - "CANCEL_SCAN": "CANCEL_SCAN", - "DESELECT_SCAN": "DESELECT_SCAN", - "EDIT_SCAN_HIDE": "EDIT_SCAN_HIDE", - "EDIT_SCAN_SHOW": "EDIT_SCAN_SHOW", - "EXPANDED_SCAN": "EXPANDED_SCAN", - "GET_SCANS": "GET_SCANS", - "GET_SCAN_CONNECTION_RESULTS": "GET_SCAN_CONNECTION_RESULTS", - "GET_SCAN_INSPECTION_RESULTS": "GET_SCAN_INSPECTION_RESULTS", - "GET_SCAN_JOB": "GET_SCAN_JOB", - "GET_SCAN_JOBS": "GET_SCAN_JOBS", - "MERGE_SCAN_DIALOG_HIDE": "MERGE_SCAN_DIALOG_HIDE", - "MERGE_SCAN_DIALOG_SHOW": "MERGE_SCAN_DIALOG_SHOW", - "NOT_EXPANDED_SCAN": "NOT_EXPANDED_SCAN", - "PAUSE_SCAN": "PAUSE_SCAN", - "RESET_DELETE_SCAN": "RESET_DELETE_SCAN", - "RESTART_SCAN": "RESTART_SCAN", - "SELECT_SCAN": "SELECT_SCAN", - "START_SCAN": "START_SCAN", - "UPDATE_SCANS": "UPDATE_SCANS", - }, - "scansTypes": Object { - "ADD_SCAN": "ADD_SCAN", - "ADD_START_SCAN": "ADD_START_SCAN", - "CANCEL_SCAN": "CANCEL_SCAN", - "DESELECT_SCAN": "DESELECT_SCAN", - "EDIT_SCAN_HIDE": "EDIT_SCAN_HIDE", - "EDIT_SCAN_SHOW": "EDIT_SCAN_SHOW", - "EXPANDED_SCAN": "EXPANDED_SCAN", - "GET_SCANS": "GET_SCANS", - "GET_SCAN_CONNECTION_RESULTS": "GET_SCAN_CONNECTION_RESULTS", - "GET_SCAN_INSPECTION_RESULTS": "GET_SCAN_INSPECTION_RESULTS", - "GET_SCAN_JOB": "GET_SCAN_JOB", - "GET_SCAN_JOBS": "GET_SCAN_JOBS", - "MERGE_SCAN_DIALOG_HIDE": "MERGE_SCAN_DIALOG_HIDE", - "MERGE_SCAN_DIALOG_SHOW": "MERGE_SCAN_DIALOG_SHOW", - "NOT_EXPANDED_SCAN": "NOT_EXPANDED_SCAN", - "PAUSE_SCAN": "PAUSE_SCAN", - "RESET_DELETE_SCAN": "RESET_DELETE_SCAN", - "RESTART_SCAN": "RESTART_SCAN", - "SELECT_SCAN": "SELECT_SCAN", - "START_SCAN": "START_SCAN", - "UPDATE_SCANS": "UPDATE_SCANS", - }, }, "sources": Object { "ADD_SOURCE": "ADD_SOURCE", @@ -197,15 +165,22 @@ Object { }, "credentials": Object { "ADD_CREDENTIAL": "ADD_CREDENTIAL", + "CONFIRM_DELETE_CREDENTIAL": "CONFIRM_DELETE_CREDENTIAL", "CREATE_CREDENTIAL_SHOW": "CREATE_CREDENTIAL_SHOW", "DELETE_CREDENTIAL": "DELETE_CREDENTIAL", "DELETE_CREDENTIALS": "DELETE_CREDENTIALS", + "DESELECT_CREDENTIAL": "DESELECT_CREDENTIAL", "EDIT_CREDENTIAL_SHOW": "EDIT_CREDENTIAL_SHOW", + "EXPANDED_CREDENTIAL": "EXPANDED_CREDENTIAL", "GET_CREDENTIAL": "GET_CREDENTIAL", "GET_CREDENTIALS": "GET_CREDENTIALS", "GET_WIZARD_CREDENTIALS": "GET_WIZARD_CREDENTIALS", + "NOT_EXPANDED_CREDENTIAL": "NOT_EXPANDED_CREDENTIAL", "RESET_CREDENTIAL_UPDATE_STATUS": "RESET_CREDENTIAL_UPDATE_STATUS", + "RESET_DELETE_CREDENTIAL": "RESET_DELETE_CREDENTIAL", + "SELECT_CREDENTIAL": "SELECT_CREDENTIAL", "UPDATE_CREDENTIAL": "UPDATE_CREDENTIAL", + "UPDATE_CREDENTIALS": "UPDATE_CREDENTIALS", "UPDATE_CREDENTIAL_HIDE": "UPDATE_CREDENTIAL_HIDE", }, "facts": Object { @@ -238,52 +213,6 @@ Object { "SELECT_SCAN": "SELECT_SCAN", "START_SCAN": "START_SCAN", "UPDATE_SCANS": "UPDATE_SCANS", - "default": Object { - "ADD_SCAN": "ADD_SCAN", - "ADD_START_SCAN": "ADD_START_SCAN", - "CANCEL_SCAN": "CANCEL_SCAN", - "DESELECT_SCAN": "DESELECT_SCAN", - "EDIT_SCAN_HIDE": "EDIT_SCAN_HIDE", - "EDIT_SCAN_SHOW": "EDIT_SCAN_SHOW", - "EXPANDED_SCAN": "EXPANDED_SCAN", - "GET_SCANS": "GET_SCANS", - "GET_SCAN_CONNECTION_RESULTS": "GET_SCAN_CONNECTION_RESULTS", - "GET_SCAN_INSPECTION_RESULTS": "GET_SCAN_INSPECTION_RESULTS", - "GET_SCAN_JOB": "GET_SCAN_JOB", - "GET_SCAN_JOBS": "GET_SCAN_JOBS", - "MERGE_SCAN_DIALOG_HIDE": "MERGE_SCAN_DIALOG_HIDE", - "MERGE_SCAN_DIALOG_SHOW": "MERGE_SCAN_DIALOG_SHOW", - "NOT_EXPANDED_SCAN": "NOT_EXPANDED_SCAN", - "PAUSE_SCAN": "PAUSE_SCAN", - "RESET_DELETE_SCAN": "RESET_DELETE_SCAN", - "RESTART_SCAN": "RESTART_SCAN", - "SELECT_SCAN": "SELECT_SCAN", - "START_SCAN": "START_SCAN", - "UPDATE_SCANS": "UPDATE_SCANS", - }, - "scansTypes": Object { - "ADD_SCAN": "ADD_SCAN", - "ADD_START_SCAN": "ADD_START_SCAN", - "CANCEL_SCAN": "CANCEL_SCAN", - "DESELECT_SCAN": "DESELECT_SCAN", - "EDIT_SCAN_HIDE": "EDIT_SCAN_HIDE", - "EDIT_SCAN_SHOW": "EDIT_SCAN_SHOW", - "EXPANDED_SCAN": "EXPANDED_SCAN", - "GET_SCANS": "GET_SCANS", - "GET_SCAN_CONNECTION_RESULTS": "GET_SCAN_CONNECTION_RESULTS", - "GET_SCAN_INSPECTION_RESULTS": "GET_SCAN_INSPECTION_RESULTS", - "GET_SCAN_JOB": "GET_SCAN_JOB", - "GET_SCAN_JOBS": "GET_SCAN_JOBS", - "MERGE_SCAN_DIALOG_HIDE": "MERGE_SCAN_DIALOG_HIDE", - "MERGE_SCAN_DIALOG_SHOW": "MERGE_SCAN_DIALOG_SHOW", - "NOT_EXPANDED_SCAN": "NOT_EXPANDED_SCAN", - "PAUSE_SCAN": "PAUSE_SCAN", - "RESET_DELETE_SCAN": "RESET_DELETE_SCAN", - "RESTART_SCAN": "RESTART_SCAN", - "SELECT_SCAN": "SELECT_SCAN", - "START_SCAN": "START_SCAN", - "UPDATE_SCANS": "UPDATE_SCANS", - }, }, "sources": Object { "ADD_SOURCE": "ADD_SOURCE", @@ -371,52 +300,6 @@ Object { "SELECT_SCAN": "SELECT_SCAN", "START_SCAN": "START_SCAN", "UPDATE_SCANS": "UPDATE_SCANS", - "default": Object { - "ADD_SCAN": "ADD_SCAN", - "ADD_START_SCAN": "ADD_START_SCAN", - "CANCEL_SCAN": "CANCEL_SCAN", - "DESELECT_SCAN": "DESELECT_SCAN", - "EDIT_SCAN_HIDE": "EDIT_SCAN_HIDE", - "EDIT_SCAN_SHOW": "EDIT_SCAN_SHOW", - "EXPANDED_SCAN": "EXPANDED_SCAN", - "GET_SCANS": "GET_SCANS", - "GET_SCAN_CONNECTION_RESULTS": "GET_SCAN_CONNECTION_RESULTS", - "GET_SCAN_INSPECTION_RESULTS": "GET_SCAN_INSPECTION_RESULTS", - "GET_SCAN_JOB": "GET_SCAN_JOB", - "GET_SCAN_JOBS": "GET_SCAN_JOBS", - "MERGE_SCAN_DIALOG_HIDE": "MERGE_SCAN_DIALOG_HIDE", - "MERGE_SCAN_DIALOG_SHOW": "MERGE_SCAN_DIALOG_SHOW", - "NOT_EXPANDED_SCAN": "NOT_EXPANDED_SCAN", - "PAUSE_SCAN": "PAUSE_SCAN", - "RESET_DELETE_SCAN": "RESET_DELETE_SCAN", - "RESTART_SCAN": "RESTART_SCAN", - "SELECT_SCAN": "SELECT_SCAN", - "START_SCAN": "START_SCAN", - "UPDATE_SCANS": "UPDATE_SCANS", - }, - "scansTypes": Object { - "ADD_SCAN": "ADD_SCAN", - "ADD_START_SCAN": "ADD_START_SCAN", - "CANCEL_SCAN": "CANCEL_SCAN", - "DESELECT_SCAN": "DESELECT_SCAN", - "EDIT_SCAN_HIDE": "EDIT_SCAN_HIDE", - "EDIT_SCAN_SHOW": "EDIT_SCAN_SHOW", - "EXPANDED_SCAN": "EXPANDED_SCAN", - "GET_SCANS": "GET_SCANS", - "GET_SCAN_CONNECTION_RESULTS": "GET_SCAN_CONNECTION_RESULTS", - "GET_SCAN_INSPECTION_RESULTS": "GET_SCAN_INSPECTION_RESULTS", - "GET_SCAN_JOB": "GET_SCAN_JOB", - "GET_SCAN_JOBS": "GET_SCAN_JOBS", - "MERGE_SCAN_DIALOG_HIDE": "MERGE_SCAN_DIALOG_HIDE", - "MERGE_SCAN_DIALOG_SHOW": "MERGE_SCAN_DIALOG_SHOW", - "NOT_EXPANDED_SCAN": "NOT_EXPANDED_SCAN", - "PAUSE_SCAN": "PAUSE_SCAN", - "RESET_DELETE_SCAN": "RESET_DELETE_SCAN", - "RESTART_SCAN": "RESTART_SCAN", - "SELECT_SCAN": "SELECT_SCAN", - "START_SCAN": "START_SCAN", - "UPDATE_SCANS": "UPDATE_SCANS", - }, }, "sourcesTypes": Object { "ADD_SOURCE": "ADD_SOURCE", @@ -492,15 +375,22 @@ Object { }, "credentials": Object { "ADD_CREDENTIAL": "ADD_CREDENTIAL", + "CONFIRM_DELETE_CREDENTIAL": "CONFIRM_DELETE_CREDENTIAL", "CREATE_CREDENTIAL_SHOW": "CREATE_CREDENTIAL_SHOW", "DELETE_CREDENTIAL": "DELETE_CREDENTIAL", "DELETE_CREDENTIALS": "DELETE_CREDENTIALS", + "DESELECT_CREDENTIAL": "DESELECT_CREDENTIAL", "EDIT_CREDENTIAL_SHOW": "EDIT_CREDENTIAL_SHOW", + "EXPANDED_CREDENTIAL": "EXPANDED_CREDENTIAL", "GET_CREDENTIAL": "GET_CREDENTIAL", "GET_CREDENTIALS": "GET_CREDENTIALS", "GET_WIZARD_CREDENTIALS": "GET_WIZARD_CREDENTIALS", + "NOT_EXPANDED_CREDENTIAL": "NOT_EXPANDED_CREDENTIAL", "RESET_CREDENTIAL_UPDATE_STATUS": "RESET_CREDENTIAL_UPDATE_STATUS", + "RESET_DELETE_CREDENTIAL": "RESET_DELETE_CREDENTIAL", + "SELECT_CREDENTIAL": "SELECT_CREDENTIAL", "UPDATE_CREDENTIAL": "UPDATE_CREDENTIAL", + "UPDATE_CREDENTIALS": "UPDATE_CREDENTIALS", "UPDATE_CREDENTIAL_HIDE": "UPDATE_CREDENTIAL_HIDE", }, "facts": Object { @@ -533,52 +423,6 @@ Object { "SELECT_SCAN": "SELECT_SCAN", "START_SCAN": "START_SCAN", "UPDATE_SCANS": "UPDATE_SCANS", - "default": Object { - "ADD_SCAN": "ADD_SCAN", - "ADD_START_SCAN": "ADD_START_SCAN", - "CANCEL_SCAN": "CANCEL_SCAN", - "DESELECT_SCAN": "DESELECT_SCAN", - "EDIT_SCAN_HIDE": "EDIT_SCAN_HIDE", - "EDIT_SCAN_SHOW": "EDIT_SCAN_SHOW", - "EXPANDED_SCAN": "EXPANDED_SCAN", - "GET_SCANS": "GET_SCANS", - "GET_SCAN_CONNECTION_RESULTS": "GET_SCAN_CONNECTION_RESULTS", - "GET_SCAN_INSPECTION_RESULTS": "GET_SCAN_INSPECTION_RESULTS", - "GET_SCAN_JOB": "GET_SCAN_JOB", - "GET_SCAN_JOBS": "GET_SCAN_JOBS", - "MERGE_SCAN_DIALOG_HIDE": "MERGE_SCAN_DIALOG_HIDE", - "MERGE_SCAN_DIALOG_SHOW": "MERGE_SCAN_DIALOG_SHOW", - "NOT_EXPANDED_SCAN": "NOT_EXPANDED_SCAN", - "PAUSE_SCAN": "PAUSE_SCAN", - "RESET_DELETE_SCAN": "RESET_DELETE_SCAN", - "RESTART_SCAN": "RESTART_SCAN", - "SELECT_SCAN": "SELECT_SCAN", - "START_SCAN": "START_SCAN", - "UPDATE_SCANS": "UPDATE_SCANS", - }, - "scansTypes": Object { - "ADD_SCAN": "ADD_SCAN", - "ADD_START_SCAN": "ADD_START_SCAN", - "CANCEL_SCAN": "CANCEL_SCAN", - "DESELECT_SCAN": "DESELECT_SCAN", - "EDIT_SCAN_HIDE": "EDIT_SCAN_HIDE", - "EDIT_SCAN_SHOW": "EDIT_SCAN_SHOW", - "EXPANDED_SCAN": "EXPANDED_SCAN", - "GET_SCANS": "GET_SCANS", - "GET_SCAN_CONNECTION_RESULTS": "GET_SCAN_CONNECTION_RESULTS", - "GET_SCAN_INSPECTION_RESULTS": "GET_SCAN_INSPECTION_RESULTS", - "GET_SCAN_JOB": "GET_SCAN_JOB", - "GET_SCAN_JOBS": "GET_SCAN_JOBS", - "MERGE_SCAN_DIALOG_HIDE": "MERGE_SCAN_DIALOG_HIDE", - "MERGE_SCAN_DIALOG_SHOW": "MERGE_SCAN_DIALOG_SHOW", - "NOT_EXPANDED_SCAN": "NOT_EXPANDED_SCAN", - "PAUSE_SCAN": "PAUSE_SCAN", - "RESET_DELETE_SCAN": "RESET_DELETE_SCAN", - "RESTART_SCAN": "RESTART_SCAN", - "SELECT_SCAN": "SELECT_SCAN", - "START_SCAN": "START_SCAN", - "UPDATE_SCANS": "UPDATE_SCANS", - }, }, "sources": Object { "ADD_SOURCE": "ADD_SOURCE", diff --git a/src/redux/constants/credentialsConstants.js b/src/redux/constants/credentialsConstants.js index fff49f17..a0e8b9d1 100644 --- a/src/redux/constants/credentialsConstants.js +++ b/src/redux/constants/credentialsConstants.js @@ -10,7 +10,38 @@ const EDIT_CREDENTIAL_SHOW = 'EDIT_CREDENTIAL_SHOW'; const UPDATE_CREDENTIAL_HIDE = 'UPDATE_CREDENTIAL_HIDE'; const RESET_CREDENTIAL_UPDATE_STATUS = 'RESET_CREDENTIAL_UPDATE_STATUS'; +const SELECT_CREDENTIAL = 'SELECT_CREDENTIAL'; +const DESELECT_CREDENTIAL = 'DESELECT_CREDENTIAL'; +const EXPANDED_CREDENTIAL = 'EXPANDED_CREDENTIAL'; +const NOT_EXPANDED_CREDENTIAL = 'NOT_EXPANDED_CREDENTIAL'; +const UPDATE_CREDENTIALS = 'UPDATE_CREDENTIALS'; +const CONFIRM_DELETE_CREDENTIAL = 'CONFIRM_DELETE_CREDENTIAL'; +const RESET_DELETE_CREDENTIAL = 'RESET_DELETE_CREDENTIAL'; + +const credentialsTypes = { + ADD_CREDENTIAL, + UPDATE_CREDENTIAL, + DELETE_CREDENTIAL, + DELETE_CREDENTIALS, + GET_CREDENTIAL, + GET_CREDENTIALS, + GET_WIZARD_CREDENTIALS, + CREATE_CREDENTIAL_SHOW, + EDIT_CREDENTIAL_SHOW, + UPDATE_CREDENTIAL_HIDE, + RESET_CREDENTIAL_UPDATE_STATUS, + SELECT_CREDENTIAL, + DESELECT_CREDENTIAL, + EXPANDED_CREDENTIAL, + NOT_EXPANDED_CREDENTIAL, + UPDATE_CREDENTIALS, + CONFIRM_DELETE_CREDENTIAL, + RESET_DELETE_CREDENTIAL +}; + export { + credentialsTypes as default, + credentialsTypes, ADD_CREDENTIAL, UPDATE_CREDENTIAL, DELETE_CREDENTIAL, @@ -21,5 +52,12 @@ export { CREATE_CREDENTIAL_SHOW, EDIT_CREDENTIAL_SHOW, UPDATE_CREDENTIAL_HIDE, - RESET_CREDENTIAL_UPDATE_STATUS + RESET_CREDENTIAL_UPDATE_STATUS, + SELECT_CREDENTIAL, + DESELECT_CREDENTIAL, + EXPANDED_CREDENTIAL, + NOT_EXPANDED_CREDENTIAL, + UPDATE_CREDENTIALS, + CONFIRM_DELETE_CREDENTIAL, + RESET_DELETE_CREDENTIAL }; diff --git a/src/redux/constants/index.js b/src/redux/constants/index.js index f553b246..1ae866d5 100644 --- a/src/redux/constants/index.js +++ b/src/redux/constants/index.js @@ -1,9 +1,9 @@ import * as aboutModalTypes from './aboutModalConstants'; import * as confirmationModalTypes from './confirmationModalConstants'; -import * as credentialsTypes from './credentialsConstants'; +import { credentialsTypes } from './credentialsConstants'; import * as factsTypes from './factsConstants'; import * as reportsTypes from './reportsConstants'; -import * as scansTypes from './scansConstants'; +import { scansTypes } from './scansConstants'; import { sourcesTypes } from './sourcesConstants'; import * as statusTypes from './statusConstants'; import * as toastNotificationTypes from './toasNotificationConstants'; diff --git a/src/redux/reducers/__tests__/__snapshots__/credentialsReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/credentialsReducer.test.js.snap index 638aa4b1..d8a8c0ac 100644 --- a/src/redux/reducers/__tests__/__snapshots__/credentialsReducer.test.js.snap +++ b/src/redux/reducers/__tests__/__snapshots__/credentialsReducer.test.js.snap @@ -3,25 +3,28 @@ exports[`CredentialsReducer should handle all defined error types: rejected types ADD_CREDENTIAL 1`] = ` Object { "result": Object { - "update": Object { - "add": true, - "credential": null, - "credentialType": "", - "delete": false, + "action": Object {}, + "deleted": Object {}, + "dialog": Object { + "add": false, + "credential": undefined, + "credentialType": undefined, "edit": false, "error": true, "errorMessage": "ERROR", + "errorStatus": 0, "fulfilled": false, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": false, "show": false, + "update": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, "type": "ADD_CREDENTIAL_REJECTED", } @@ -30,78 +33,59 @@ Object { exports[`CredentialsReducer should handle all defined error types: rejected types DELETE_CREDENTIAL 1`] = ` Object { "result": Object { - "update": Object { - "add": false, - "credential": null, - "credentialType": "", - "delete": true, - "edit": false, + "action": Object {}, + "deleted": Object { "error": true, "errorMessage": "ERROR", + "errorStatus": 0, "fulfilled": false, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": false, - "show": false, + "update": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, - }, - "type": "DELETE_CREDENTIAL_REJECTED", -} -`; - -exports[`CredentialsReducer should handle all defined error types: rejected types DELETE_CREDENTIALS 1`] = ` -Object { - "result": Object { - "update": Object { + "dialog": Object { "add": false, - "credential": null, - "credentialType": "", - "delete": true, + "credential": undefined, + "credentialType": undefined, "edit": false, - "error": true, - "errorMessage": "ERROR", - "fulfilled": false, - "pending": false, "show": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, - "type": "DELETE_CREDENTIALS_REJECTED", + "type": "DELETE_CREDENTIAL_REJECTED", } `; exports[`CredentialsReducer should handle all defined error types: rejected types GET_CREDENTIALS 1`] = ` Object { "result": Object { - "update": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { "add": false, - "credential": null, - "credentialType": "", - "delete": false, + "credential": undefined, + "credentialType": undefined, "edit": false, - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, "show": false, }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, "view": Object { - "credentials": Array [], "error": true, "errorMessage": "ERROR", + "errorStatus": 0, "fulfilled": false, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": false, + "update": false, }, }, "type": "GET_CREDENTIALS_REJECTED", @@ -111,25 +95,28 @@ Object { exports[`CredentialsReducer should handle all defined error types: rejected types UPDATE_CREDENTIAL 1`] = ` Object { "result": Object { - "update": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { "add": false, - "credential": null, - "credentialType": "", - "delete": false, - "edit": true, + "credential": undefined, + "credentialType": undefined, + "edit": false, "error": true, "errorMessage": "ERROR", + "errorStatus": 0, "fulfilled": false, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": false, "show": false, + "update": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, "type": "UPDATE_CREDENTIAL_REJECTED", } @@ -138,27 +125,31 @@ Object { exports[`CredentialsReducer should handle all defined fulfilled types: fulfilled types ADD_CREDENTIAL 1`] = ` Object { "result": Object { - "update": Object { - "add": true, - "credential": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { + "add": false, + "credential": undefined, + "credentialType": undefined, + "data": Object { "test": "success", }, - "credentialType": "", - "delete": false, + "date": undefined, "edit": false, "error": false, "errorMessage": "", "fulfilled": true, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": false, "show": false, + "update": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, "type": "ADD_CREDENTIAL_FULFILLED", } @@ -167,78 +158,65 @@ Object { exports[`CredentialsReducer should handle all defined fulfilled types: fulfilled types DELETE_CREDENTIAL 1`] = ` Object { "result": Object { - "update": Object { - "add": false, - "credential": null, - "credentialType": "", - "delete": true, - "edit": false, + "action": Object {}, + "deleted": Object { + "data": Object { + "test": "success", + }, + "date": undefined, "error": false, "errorMessage": "", "fulfilled": true, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": false, - "show": false, + "update": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, - }, - "type": "DELETE_CREDENTIAL_FULFILLED", -} -`; - -exports[`CredentialsReducer should handle all defined fulfilled types: fulfilled types DELETE_CREDENTIALS 1`] = ` -Object { - "result": Object { - "update": Object { + "dialog": Object { "add": false, - "credential": null, - "credentialType": "", - "delete": true, + "credential": undefined, + "credentialType": undefined, "edit": false, - "error": false, - "errorMessage": "", - "fulfilled": true, - "pending": false, "show": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, - "type": "DELETE_CREDENTIALS_FULFILLED", + "type": "DELETE_CREDENTIAL_FULFILLED", } `; exports[`CredentialsReducer should handle all defined fulfilled types: fulfilled types GET_CREDENTIALS 1`] = ` Object { "result": Object { - "update": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { "add": false, - "credential": null, - "credentialType": "", - "delete": false, + "credential": undefined, + "credentialType": undefined, "edit": false, - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, "show": false, }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, "view": Object { - "credentials": Array [], + "data": Object { + "test": "success", + }, + "date": undefined, "error": false, "errorMessage": "", "fulfilled": true, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": false, + "update": false, }, }, "type": "GET_CREDENTIALS_FULFILLED", @@ -248,27 +226,31 @@ Object { exports[`CredentialsReducer should handle all defined fulfilled types: fulfilled types UPDATE_CREDENTIAL 1`] = ` Object { "result": Object { - "update": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { "add": false, - "credential": Object { + "credential": undefined, + "credentialType": undefined, + "data": Object { "test": "success", }, - "credentialType": "", - "delete": false, - "edit": true, + "date": undefined, + "edit": false, "error": false, "errorMessage": "", "fulfilled": true, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": false, "show": false, + "update": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, "type": "UPDATE_CREDENTIAL_FULFILLED", } @@ -277,25 +259,27 @@ Object { exports[`CredentialsReducer should handle all defined pending types: pending types ADD_CREDENTIAL 1`] = ` Object { "result": Object { - "update": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { "add": false, - "credential": null, - "credentialType": "", - "delete": false, + "credential": undefined, + "credentialType": undefined, "edit": false, "error": false, "errorMessage": "", "fulfilled": false, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": true, "show": false, + "update": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, "type": "ADD_CREDENTIAL_PENDING", } @@ -304,78 +288,57 @@ Object { exports[`CredentialsReducer should handle all defined pending types: pending types DELETE_CREDENTIAL 1`] = ` Object { "result": Object { - "update": Object { - "add": false, - "credential": null, - "credentialType": "", - "delete": true, - "edit": false, + "action": Object {}, + "deleted": Object { "error": false, "errorMessage": "", "fulfilled": false, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": true, - "show": false, - }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, + "update": false, }, - }, - "type": "DELETE_CREDENTIAL_PENDING", -} -`; - -exports[`CredentialsReducer should handle all defined pending types: pending types DELETE_CREDENTIALS 1`] = ` -Object { - "result": Object { - "update": Object { + "dialog": Object { "add": false, - "credential": null, - "credentialType": "", - "delete": true, + "credential": undefined, + "credentialType": undefined, "edit": false, - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": true, "show": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, - "type": "DELETE_CREDENTIALS_PENDING", + "type": "DELETE_CREDENTIAL_PENDING", } `; exports[`CredentialsReducer should handle all defined pending types: pending types GET_CREDENTIALS 1`] = ` Object { "result": Object { - "update": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { "add": false, - "credential": null, - "credentialType": "", - "delete": false, + "credential": undefined, + "credentialType": undefined, "edit": false, - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, "show": false, }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, "view": Object { - "credentials": Array [], "error": false, "errorMessage": "", "fulfilled": false, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": true, + "update": false, }, }, "type": "GET_CREDENTIALS_PENDING", @@ -385,25 +348,27 @@ Object { exports[`CredentialsReducer should handle all defined pending types: pending types UPDATE_CREDENTIAL 1`] = ` Object { "result": Object { - "update": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { "add": false, - "credential": null, - "credentialType": "", - "delete": false, + "credential": undefined, + "credentialType": undefined, "edit": false, "error": false, "errorMessage": "", "fulfilled": false, + "metaData": undefined, + "metaId": undefined, + "metaQuery": undefined, "pending": true, "show": false, + "update": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, "type": "UPDATE_CREDENTIAL_PENDING", } @@ -412,80 +377,175 @@ Object { exports[`CredentialsReducer should handle specific defined types: defined type CREATE_CREDENTIAL_SHOW 1`] = ` Object { "result": Object { - "update": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { "add": true, - "credential": null, + "credential": undefined, "credentialType": undefined, - "delete": false, "edit": false, - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, "show": true, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, "type": "CREATE_CREDENTIAL_SHOW", } `; +exports[`CredentialsReducer should handle specific defined types: defined type DESELECT_CREDENTIAL 1`] = ` +Object { + "result": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { + "add": false, + "credential": undefined, + "credentialType": undefined, + "edit": false, + "show": false, + }, + "expanded": Object {}, + "selected": Object { + "undefined": null, + }, + "update": 0, + "view": Object {}, + }, + "type": "DESELECT_CREDENTIAL", +} +`; + exports[`CredentialsReducer should handle specific defined types: defined type EDIT_CREDENTIAL_SHOW 1`] = ` Object { "result": Object { - "update": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { "add": false, "credential": undefined, - "credentialType": "", - "delete": false, + "credentialType": undefined, "edit": true, - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, "show": true, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, - }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, "type": "EDIT_CREDENTIAL_SHOW", } `; -exports[`CredentialsReducer should handle specific defined types: defined type UPDATE_CREDENTIAL_HIDE 1`] = ` +exports[`CredentialsReducer should handle specific defined types: defined type EXPANDED_CREDENTIAL 1`] = ` Object { "result": Object { - "update": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { "add": false, - "credential": null, - "credentialType": "", - "delete": false, + "credential": undefined, + "credentialType": undefined, "edit": false, - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, "show": false, }, - "view": Object { - "credentials": Array [], - "error": false, - "errorMessage": "", - "fulfilled": false, - "pending": false, + "expanded": Object { + "undefined": undefined, }, + "selected": Object {}, + "update": 0, + "view": Object {}, + }, + "type": "EXPANDED_CREDENTIAL", +} +`; + +exports[`CredentialsReducer should handle specific defined types: defined type NOT_EXPANDED_CREDENTIAL 1`] = ` +Object { + "result": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { + "add": false, + "credential": undefined, + "credentialType": undefined, + "edit": false, + "show": false, + }, + "expanded": Object { + "undefined": null, + }, + "selected": Object {}, + "update": 0, + "view": Object {}, + }, + "type": "NOT_EXPANDED_CREDENTIAL", +} +`; + +exports[`CredentialsReducer should handle specific defined types: defined type SELECT_CREDENTIAL 1`] = ` +Object { + "result": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { + "add": false, + "credential": undefined, + "credentialType": undefined, + "edit": false, + "show": false, + }, + "expanded": Object {}, + "selected": Object { + "undefined": undefined, + }, + "update": 0, + "view": Object {}, + }, + "type": "SELECT_CREDENTIAL", +} +`; + +exports[`CredentialsReducer should handle specific defined types: defined type UPDATE_CREDENTIAL_HIDE 1`] = ` +Object { + "result": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { + "add": false, + "credential": undefined, + "credentialType": undefined, + "edit": false, + "show": false, + }, + "expanded": Object {}, + "selected": Object {}, + "update": 0, + "view": Object {}, }, "type": "UPDATE_CREDENTIAL_HIDE", } `; + +exports[`CredentialsReducer should handle specific defined types: defined type UPDATE_CREDENTIALS 1`] = ` +Object { + "result": Object { + "action": Object {}, + "deleted": Object {}, + "dialog": Object { + "add": false, + "credential": undefined, + "credentialType": undefined, + "edit": false, + "show": false, + }, + "expanded": Object {}, + "selected": Object {}, + "update": 1654041600000, + "view": Object {}, + }, + "type": "UPDATE_CREDENTIALS", +} +`; diff --git a/src/redux/reducers/__tests__/credentialsReducer.test.js b/src/redux/reducers/__tests__/credentialsReducer.test.js index 8c2d4ecb..f9b17ebe 100644 --- a/src/redux/reducers/__tests__/credentialsReducer.test.js +++ b/src/redux/reducers/__tests__/credentialsReducer.test.js @@ -1,6 +1,6 @@ import credentialsReducer from '../credentialsReducer'; import { credentialsTypes as types } from '../../constants'; -import { reduxHelpers } from '../../common/reduxHelpers'; +import { reduxHelpers } from '../../common'; describe('CredentialsReducer', () => { it('should return the initial state', () => { @@ -8,7 +8,16 @@ describe('CredentialsReducer', () => { }); it('should handle specific defined types', () => { - const specificTypes = [types.CREATE_CREDENTIAL_SHOW, types.EDIT_CREDENTIAL_SHOW, types.UPDATE_CREDENTIAL_HIDE]; + const specificTypes = [ + types.UPDATE_CREDENTIALS, + types.SELECT_CREDENTIAL, + types.DESELECT_CREDENTIAL, + types.EXPANDED_CREDENTIAL, + types.NOT_EXPANDED_CREDENTIAL, + types.CREATE_CREDENTIAL_SHOW, + types.EDIT_CREDENTIAL_SHOW, + types.UPDATE_CREDENTIAL_HIDE + ]; specificTypes.forEach(value => { const dispatched = { @@ -25,7 +34,6 @@ describe('CredentialsReducer', () => { const specificTypes = [ types.ADD_CREDENTIAL, types.DELETE_CREDENTIAL, - types.DELETE_CREDENTIALS, types.UPDATE_CREDENTIAL, types.GET_CREDENTIALS ]; @@ -58,7 +66,6 @@ describe('CredentialsReducer', () => { const specificTypes = [ types.ADD_CREDENTIAL, types.DELETE_CREDENTIAL, - types.DELETE_CREDENTIALS, types.UPDATE_CREDENTIAL, types.GET_CREDENTIALS ]; @@ -80,7 +87,6 @@ describe('CredentialsReducer', () => { const specificTypes = [ types.ADD_CREDENTIAL, types.DELETE_CREDENTIAL, - types.DELETE_CREDENTIALS, types.UPDATE_CREDENTIAL, types.GET_CREDENTIALS ]; diff --git a/src/redux/reducers/credentialsReducer.js b/src/redux/reducers/credentialsReducer.js index b0b4ffab..ebf14b66 100644 --- a/src/redux/reducers/credentialsReducer.js +++ b/src/redux/reducers/credentialsReducer.js @@ -1,168 +1,80 @@ import { credentialsTypes } from '../constants'; -import { helpers } from '../../common/helpers'; -import { reduxHelpers } from '../common/reduxHelpers'; -import apiTypes from '../../constants/apiConstants'; +import { reduxHelpers } from '../common'; +import { helpers } from '../../common'; const initialState = { - view: { - error: false, - errorMessage: '', - pending: false, - fulfilled: false, - credentials: [] - }, - update: { - error: false, - errorMessage: '', - pending: false, - fulfilled: false, - credential: null, - credentialType: '', + deleted: {}, + dialog: { show: false, add: false, edit: false, - delete: false - } + credentialType: undefined, + credential: undefined + }, + action: {}, + selected: {}, + expanded: {}, + update: 0, + view: {} }; const credentialsReducer = (state = initialState, action) => { switch (action.type) { - case credentialsTypes.CREATE_CREDENTIAL_SHOW: + case credentialsTypes.UPDATE_CREDENTIALS: return reduxHelpers.setStateProp( - 'update', + null, { - show: true, - add: true, - credentialType: action.credentialType - }, - { - state, - initialState - } - ); - - case credentialsTypes.EDIT_CREDENTIAL_SHOW: - return reduxHelpers.setStateProp( - 'update', - { - show: true, - edit: true, - credential: action.credential - }, - { - state, - initialState - } - ); - - case credentialsTypes.UPDATE_CREDENTIAL_HIDE: - return reduxHelpers.setStateProp( - 'update', - { - show: false - }, - { - state, - initialState - } - ); - - case reduxHelpers.REJECTED_ACTION(credentialsTypes.ADD_CREDENTIAL): - return reduxHelpers.setStateProp( - 'update', - { - add: true, - error: action.error, - errorMessage: helpers.getMessageFromResults(action.payload).message, - pending: false + update: helpers.getCurrentDate().getTime() }, { state, reset: false } ); - - case reduxHelpers.REJECTED_ACTION(credentialsTypes.DELETE_CREDENTIAL): - case reduxHelpers.REJECTED_ACTION(credentialsTypes.DELETE_CREDENTIALS): + case credentialsTypes.SELECT_CREDENTIAL: return reduxHelpers.setStateProp( - 'update', + 'selected', { - error: action.error, - errorMessage: helpers.getMessageFromResults(action.payload).message, - delete: true, - pending: false + [action.item?.id]: action.item }, { state, reset: false } ); + case credentialsTypes.DESELECT_CREDENTIAL: + const itemsToDeselect = {}; + const deselectItems = (Array.isArray(action.item) && action.item) || [action?.item || {}]; + deselectItems.forEach(({ id }) => { + itemsToDeselect[id] = null; + }); - case reduxHelpers.REJECTED_ACTION(credentialsTypes.UPDATE_CREDENTIAL): return reduxHelpers.setStateProp( - 'update', + 'selected', { - error: action.error, - errorMessage: helpers.getMessageFromResults(action.payload).message, - pending: false, - edit: true + ...itemsToDeselect }, { state, reset: false } ); - - case reduxHelpers.REJECTED_ACTION(credentialsTypes.GET_CREDENTIALS): + case credentialsTypes.EXPANDED_CREDENTIAL: return reduxHelpers.setStateProp( - 'view', + 'expanded', { - error: action.error, - errorMessage: helpers.getMessageFromResults(action.payload).message - }, - { - state, - initialState - } - ); - - case reduxHelpers.PENDING_ACTION(credentialsTypes.ADD_CREDENTIAL): - return reduxHelpers.setStateProp( - 'update', - { - pending: true, - error: false, - fulfilled: false - }, - { - state, - reset: false - } - ); - - case reduxHelpers.PENDING_ACTION(credentialsTypes.DELETE_CREDENTIAL): - case reduxHelpers.PENDING_ACTION(credentialsTypes.DELETE_CREDENTIALS): - return reduxHelpers.setStateProp( - 'update', - { - pending: true, - delete: true, - error: false, - fulfilled: false + [action.item?.id]: action.cellIndex }, { state, reset: false } ); - - case reduxHelpers.PENDING_ACTION(credentialsTypes.UPDATE_CREDENTIAL): + case credentialsTypes.NOT_EXPANDED_CREDENTIAL: return reduxHelpers.setStateProp( - 'update', + 'expanded', { - pending: true, - error: false, - fulfilled: false + [action.item?.id]: null }, { state, @@ -170,40 +82,13 @@ const credentialsReducer = (state = initialState, action) => { } ); - case reduxHelpers.PENDING_ACTION(credentialsTypes.GET_CREDENTIALS): - return reduxHelpers.setStateProp( - 'view', - { - pending: true, - credentials: state.view.credentials - }, - { - state, - initialState - } - ); - - case reduxHelpers.FULFILLED_ACTION(credentialsTypes.ADD_CREDENTIAL): + case credentialsTypes.CREATE_CREDENTIAL_SHOW: return reduxHelpers.setStateProp( - 'update', + 'dialog', { + show: true, add: true, - credential: action.payload.data || {}, - fulfilled: true - }, - { - state, - initialState - } - ); - - case reduxHelpers.FULFILLED_ACTION(credentialsTypes.DELETE_CREDENTIAL): - case reduxHelpers.FULFILLED_ACTION(credentialsTypes.DELETE_CREDENTIALS): - return reduxHelpers.setStateProp( - 'update', - { - delete: true, - fulfilled: true + credentialType: action.credentialType }, { state, @@ -211,13 +96,13 @@ const credentialsReducer = (state = initialState, action) => { } ); - case reduxHelpers.FULFILLED_ACTION(credentialsTypes.UPDATE_CREDENTIAL): + case credentialsTypes.EDIT_CREDENTIAL_SHOW: return reduxHelpers.setStateProp( - 'update', + 'dialog', { - credential: action.payload.data || {}, + show: true, edit: true, - fulfilled: true + credential: action.credential }, { state, @@ -225,12 +110,11 @@ const credentialsReducer = (state = initialState, action) => { } ); - case reduxHelpers.FULFILLED_ACTION(credentialsTypes.GET_CREDENTIALS): + case credentialsTypes.UPDATE_CREDENTIAL_HIDE: return reduxHelpers.setStateProp( - 'view', + 'dialog', { - credentials: (action.payload.data && action.payload.data[apiTypes.API_RESPONSE_CREDENTIALS_RESULTS]) || [], - fulfilled: true + show: false }, { state, @@ -239,7 +123,21 @@ const credentialsReducer = (state = initialState, action) => { ); default: - return state; + return reduxHelpers.generatedPromiseActionReducer( + [ + { + ref: 'deleted', + type: [credentialsTypes.DELETE_CREDENTIAL] + }, + { + ref: 'dialog', + type: [credentialsTypes.ADD_CREDENTIAL, credentialsTypes.UPDATE_CREDENTIAL] + }, + { ref: 'view', type: credentialsTypes.GET_CREDENTIALS } + ], + state, + action + ); } }; diff --git a/src/redux/selectors/__tests__/credentialsSelectors.test.js b/src/redux/selectors/__tests__/credentialsSelectors.test.js index af9e0a63..94f17ea2 100644 --- a/src/redux/selectors/__tests__/credentialsSelectors.test.js +++ b/src/redux/selectors/__tests__/credentialsSelectors.test.js @@ -10,23 +10,25 @@ describe('CredentialsSelectors', () => { const state = { credentials: { view: { - credentials: [ - { - [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: 'Lorem', - [apiTypes.API_RESPONSE_CREDENTIAL_CRED_TYPE]: 'network', - [apiTypes.API_RESPONSE_CREDENTIAL_ID]: 54 - }, - { - [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: 'Ipsum', - [apiTypes.API_RESPONSE_CREDENTIAL_CRED_TYPE]: 'vcenter', - [apiTypes.API_RESPONSE_CREDENTIAL_ID]: 1 - }, - { - [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: 'Dolor', - [apiTypes.API_RESPONSE_CREDENTIAL_CRED_TYPE]: 'satellite', - [apiTypes.API_RESPONSE_CREDENTIAL_ID]: 200 - } - ] + data: { + [apiTypes.API_RESPONSE_CREDENTIALS_RESULTS]: [ + { + [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: 'Lorem', + [apiTypes.API_RESPONSE_CREDENTIAL_CRED_TYPE]: 'network', + [apiTypes.API_RESPONSE_CREDENTIAL_ID]: 54 + }, + { + [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: 'Ipsum', + [apiTypes.API_RESPONSE_CREDENTIAL_CRED_TYPE]: 'vcenter', + [apiTypes.API_RESPONSE_CREDENTIAL_ID]: 1 + }, + { + [apiTypes.API_RESPONSE_CREDENTIAL_NAME]: 'Dolor', + [apiTypes.API_RESPONSE_CREDENTIAL_CRED_TYPE]: 'satellite', + [apiTypes.API_RESPONSE_CREDENTIAL_ID]: 200 + } + ] + } } } }; diff --git a/src/redux/selectors/credentialsSelectors.js b/src/redux/selectors/credentialsSelectors.js index 4b25d785..251b74ca 100644 --- a/src/redux/selectors/credentialsSelectors.js +++ b/src/redux/selectors/credentialsSelectors.js @@ -7,7 +7,7 @@ import apiTypes from '../../constants/apiConstants'; * @param {object} state * @returns {*} */ -const credentials = state => state.credentials.view.credentials; +const credentials = state => state.credentials.view?.data?.[apiTypes.API_RESPONSE_CREDENTIALS_RESULTS]; const credentialsDropdownSelector = createSelector([credentials], creds => (creds || []).map(cred => ({ diff --git a/src/services/__tests__/credentialsService.test.js b/src/services/__tests__/credentialsService.test.js index bd00c8e6..84463e17 100644 --- a/src/services/__tests__/credentialsService.test.js +++ b/src/services/__tests__/credentialsService.test.js @@ -17,14 +17,12 @@ describe('CredentialsService', () => { }); it('should export a specific number of methods and classes', () => { - expect(Object.keys(credentialsService)).toHaveLength(6); + expect(Object.keys(credentialsService)).toHaveLength(4); }); it('should have specific methods', () => { expect(credentialsService.addCredential).toBeDefined(); expect(credentialsService.deleteCredential).toBeDefined(); - expect(credentialsService.deleteCredentials).toBeDefined(); - expect(credentialsService.getCredential).toBeDefined(); expect(credentialsService.getCredentials).toBeDefined(); expect(credentialsService.updateCredential).toBeDefined(); }); diff --git a/src/services/credentialsService.js b/src/services/credentialsService.js index c140a13a..b442895b 100644 --- a/src/services/credentialsService.js +++ b/src/services/credentialsService.js @@ -13,9 +13,6 @@ const deleteCredential = id => url: `${process.env.REACT_APP_CREDENTIALS_SERVICE}${id}/` }); -const deleteCredentials = (data = []) => - Promise.all(data.map(id => deleteCredential(id))).then(success => new Promise(resolve => resolve({ data: success }))); - const getCredentials = (id = '', params = {}) => serviceCall( { @@ -25,8 +22,6 @@ const getCredentials = (id = '', params = {}) => { auth: false } ); -const getCredential = id => getCredentials(id); - const updateCredential = (id, data = {}) => serviceCall({ method: 'put', @@ -37,8 +32,6 @@ const updateCredential = (id, data = {}) => const credentialsService = { addCredential, deleteCredential, - deleteCredentials, - getCredential, getCredentials, updateCredential }; @@ -48,8 +41,6 @@ export { credentialsService, addCredential, deleteCredential, - deleteCredentials, - getCredential, getCredentials, updateCredential }; diff --git a/src/styles/app/_list.scss b/src/styles/app/_list.scss new file mode 100644 index 00000000..8d566a28 --- /dev/null +++ b/src/styles/app/_list.scss @@ -0,0 +1,5 @@ +.quipucords-list__overflow-scroll { + overflow-y: auto; + min-height: 4em; + max-height: 12em; +} diff --git a/src/styles/index.scss b/src/styles/index.scss index ea35ebd8..094f9df9 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -27,6 +27,7 @@ $icon-font-path: '~patternfly/dist/fonts/'; @import 'app/login'; @import 'app/app'; @import 'app/dropdownSelect'; +@import 'app/list'; @import 'app/table'; @import 'app/forms'; @import 'app/pageLayout';