diff --git a/.env b/.env
index bd0decc7..3c1a6120 100644
--- a/.env
+++ b/.env
@@ -10,6 +10,8 @@ REACT_APP_UI_BRAND=false
REACT_APP_AUTH_TOKEN=csrftoken
REACT_APP_AUTH_HEADER=X-CSRFToken
REACT_APP_AJAX_TIMEOUT=60000
+REACT_APP_TOAST_NOTIFICATIONS_TIMEOUT=8000
+REACT_APP_POLL_INTERVAL=120000
REACT_APP_CONFIG_SERVICE_LOCALES_DEFAULT_LNG=en
REACT_APP_CONFIG_SERVICE_LOCALES_DEFAULT_LNG_DESC=English
diff --git a/.eslintrc b/.eslintrc
index b468faab..752138c6 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -9,6 +9,7 @@
"plugins": [
"import",
"jest",
+ "jsdoc",
"json",
"prettier",
"react"
@@ -21,7 +22,8 @@
"airbnb",
"airbnb/hooks",
"prettier",
- "plugin:jest/recommended"
+ "plugin:jest/recommended",
+ "plugin:jsdoc/recommended"
],
"globals": {
"mountHook": "readonly",
@@ -57,6 +59,17 @@
"import/no-named-as-default-member": 0,
"jest/no-done-callback": 0,
"jest/prefer-to-have-length": 0,
+ "jsdoc/require-param-description": 0,
+ "jsdoc/require-property-description": 0,
+ "jsdoc/require-returns-description": 0,
+ "jsdoc/tag-lines": [
+ "warn",
+ "always",
+ {
+ "count": 0,
+ "noEndLines": true
+ }
+ ],
"max-len": [
"error",
{
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 20f387d9..3feb1737 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -1,7 +1,7 @@
name: Build
on:
push:
- branches: [master, main]
+ branches: [master, main, dev**]
tags:
- "*"
pull_request:
diff --git a/package.json b/package.json
index 68fae1fe..f9fd3a92 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"!src/components/app.js",
"!src/components/**/index.js",
"!src/common/index.js",
+ "!src/hooks/index.js",
"!src/redux/index.js",
"!src/redux/store.js",
"!src/redux/middleware/**",
@@ -79,9 +80,10 @@
"scripts": {
"api:build": "run-s -l build:pre build:js build:post",
"api:dev": "sh ./scripts/api.sh -p 5000 -t dev",
- "api:docs": "sh ./scripts/api.sh -t docs",
+ "api:specs": "sh ./scripts/api.sh -t specs",
"api:review": "sh ./scripts/api.sh -p 9443 -t review",
"api:stage": "sh ./scripts/api.sh -p 5001 -t stage",
+ "api:stop": "sh ./scripts/api.sh -t stopApi",
"api:update": "npm run api:build; sh ./scripts/api.sh -t update",
"build": "run-s -l build:pre build:docs test:docs build:template-css build:js build:post test:integration",
"build:clean": "bash ./scripts/clean.sh",
@@ -91,12 +93,13 @@
"build:post": "bash ./scripts/post.sh; bash ./scripts/clean.sh",
"build:pre": "bash ./scripts/pre.sh",
"build:brand": "run-s -l 'build:pre -b' 'build:docs -b' test:docs build:template-css build:js build:post test:integration",
- "build:template-css": "sass --no-source-map --load-path ./src --load-path ./node_modules ./src/styles/index.scss ./src/styles/.css/index.css",
+ "build:template-css": "sass --no-source-map --load-path ./src --load-path ./node_modules ./src/styles/template.scss ./src/styles/.css/index.css",
"release": "standard-version",
- "start": "open http://localhost:5050/docs/api; run-p -l api:dev start:js",
+ "start": "run-p -l api:dev start:js",
+ "start:specs": "open http://localhost:5050/docs/api; run-s api:specs",
"start:js": "react-scripts start",
"start:dev": "npm start",
- "start:review": "open https://localhost:9443/login/; run-s build && run-s -l api:review;",
+ "start:review": "open https://127.0.0.1:9443/login/; run-s build && run-s -l api:review;",
"start:stage": "open https://localhost:5001/login/; run-s api:build && run-p -l api:stage start:ui-stage",
"start:ui-stage": "sh -ac '. ./.env;. ./.env.staging; react-app-rewired start'",
"test": "run-s test:lint test:ci",
@@ -111,6 +114,12 @@
"test:unit": "react-scripts test --env=jsdom"
},
"dependencies": {
+ "@patternfly/patternfly": "4.196.7",
+ "@patternfly/react-core": "4.221.3",
+ "@patternfly/react-icons": "4.72.3",
+ "@patternfly/react-styles": "4.71.3",
+ "@patternfly/react-table": "4.71.37",
+ "@patternfly/react-tokens": "4.73.3",
"axios": "^0.27.2",
"bootstrap": "^5.1.3",
"classnames": "^2.3.1",
@@ -120,7 +129,7 @@
"i18next-xhr-backend": "^2.0.1",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
- "moment": "^2.29.3",
+ "moment": "^2.29.4",
"npm-run-all": "^4.1.5",
"patternfly": "3.59.5",
"patternfly-react": "2.40.0",
@@ -154,10 +163,9 @@
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^3.4.0",
- "eslint-plugin-react": "^7.30.0",
+ "eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"express": "^4.18.1",
- "gettext-extractor": "^3.5.4",
"htmlhint": "^1.1.4",
"iso-639-1": "^2.1.15",
"jest": "26.6.0",
diff --git a/public/locales/en.json b/public/locales/en.json
index 0967ef42..5679bd44 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -1 +1,205 @@
-{}
+{
+ "about-modal": {
+ "browser-os": "Browser OS",
+ "browser-version": "Browser Version",
+ "copy-button": "Copy application information",
+ "copy-confirmation": "Application information copied",
+ "copyright": "Copyright (c) {{year}} Red Hat Inc.",
+ "server-version": "Server Version",
+ "ui-version": "UI Version",
+ "username": "Username"
+ },
+ "form-dialog": {
+ "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?",
+ "confirmation_heading_add-source_edit": "Are you sure you want to cancel updating this source?",
+ "confirmation_title_add-source": "Cancel Add Source",
+ "confirmation_title_add-source_edit": "Cancel Updating Source",
+ "confirmation_body_add-source_exit": "The wizard is in a pending state and will continue adding this source.",
+ "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_title_delete-source": "Delete Source",
+ "confirmation_heading_delete-source": "Are you sure you want to delete the source <0>{{name}}0>?",
+ "empty-state_description_add-source": "{{context}}",
+ "empty-state_description_add-source_error": "There are errors on a previous step. Use the Back button to review your settings and try again.",
+ "empty-state_description_add-source_pending": "Please wait while source {{name}} is being created.",
+ "empty-state_description_add-source_pending_edit": "Please wait while source {{name}} is being updated.",
+ "empty-state_title_add-source": "{{name}} was created.",
+ "empty-state_title_add-source_edit": "{{name}} was updated.",
+ "empty-state_title_add-source_error": "Error Creating Source",
+ "empty-state_title_add-source_error_edit": "Error Updating Source",
+ "empty-state_title_add-source_pending": "Creating Source...",
+ "empty-state_title_add-source_pending_edit": "Updating Source...",
+ "label": "{{context}}",
+ "label_add": "Add",
+ "label_add-credential": "Add a credential",
+ "label_cancel": "Cancel",
+ "label_close": "Close",
+ "label_delete": "Delete",
+ "label_no": "No",
+ "label_option_disableSsl": "Disable SSL",
+ "label_option_SSLv23": "SSLv23",
+ "label_option_TLSv1": "TLSv1",
+ "label_option_TLSv1_1": "TLSv1.1",
+ "label_option_TLSv1_2": "TLSv1.2",
+ "label_option_sshKey": "SSH Key",
+ "label_option_usernamePassword": "Username and Password",
+ "label_option_network": "Network Credential",
+ "label_option_satellite": "Satellite Credential",
+ "label_option_vcenter": "VCenter Credential",
+ "label_placeholder_add-credential": "Add Credential",
+ "label_placeholder_add-source_credential": "Select a credential",
+ "label_placeholder_add-source_credential_add": "Add a credential",
+ "label_placeholder_add-source_credential_multi": "Select one or more credentials",
+ "label_placeholder_add-source_credential_multi_add": "Add one or more credentials",
+ "label_submit": "Submit",
+ "label_submit_add-source": "Save",
+ "label_submit_confirmation": "Confirm",
+ "label_submit_create-credential": "Save",
+ "label_submit_create-scan": "Scan",
+ "label_submit_merge-reports": "Merge",
+ "label_yes": "Yes",
+ "title_add-source": "Add Source",
+ "title_add-source_edit": "Edit Source",
+ "title_add-source_step": "Source Type",
+ "title_add-source_step_two": "Credentials",
+ "title_add-source_step_three": "Confirm"
+ },
+ "modal": {
+ "aria-label-default": "Application modal"
+ },
+ "refresh-time-button": {
+ "refreshed": "Refreshed just now",
+ "refreshed_load": "Refreshed {{refresh}}"
+ },
+ "table": {
+ "header_credentials": "Credentials",
+ "header_description": "Description",
+ "header_failed": "Failed systems",
+ "header_failed_sources": "Failed hosts",
+ "header_scan": "Scan",
+ "header_scan-jobs": "Previous scans",
+ "header_sources": "Sources",
+ "header_success": "Successful systems",
+ "header_success_sources": "Ok hosts",
+ "header_unreachable": "Unreachable systems",
+ "header_unreachable_sources": "Unreachable hosts",
+ "label_network-range": "Network Range",
+ "label": "{{context}}",
+ "label_action_scan": "Start scan",
+ "label_action_scan_canceled": "Retry scan",
+ "label_action_scan_cancelled": "Retry scan",
+ "label_action_scan_completed": "Run scan",
+ "label_action_scan_created": "Pause scan",
+ "label_action_scan_download": "Download",
+ "label_action_scan_failed": "Retry scan",
+ "label_action_scan_running": "Pause scan",
+ "label_action_scan_paused": "Resume scan",
+ "label_action_scan_pending": "Cancel scan",
+ "label_add": "Add",
+ "label_delete": "Delete",
+ "label_edit": "Edit",
+ "label_merge-reports": "Merge reports",
+ "label_scan": "Scan",
+ "label_status": "{{context}}",
+ "label_status_scans": "Scan created",
+ "label_status_completed": "Last completed",
+ "label_status_failed": "Failed",
+ "label_status_canceled": "Canceled",
+ "label_status_cancelled": "Canceled",
+ "label_status_created": "In progress",
+ "label_status_pending": "In progress",
+ "label_status_running": "In progress",
+ "label_status_paused": "Paused",
+ "label_status_completed_sources": "Last connected",
+ "label_status_failed_sources": "Connection failed",
+ "label_status_canceled_sources": "Connection canceled",
+ "label_status_cancelled_sources": "Connection canceled",
+ "label_status_created_sources": "Connection in progress",
+ "label_status_pending_sources": "Connection in progress",
+ "label_status_running_sources": "Connection in progress",
+ "label_status_paused_sources": "Connection paused",
+ "label_status_completed_scans": "Last scanned",
+ "label_status_failed_scans": "Scan failed",
+ "label_status_canceled_scans": "Scan canceled",
+ "label_status_cancelled_scans": "Scan canceled",
+ "label_status_created_scans": "Scan in progress",
+ "label_status_pending_scans": "Scan in progress",
+ "label_status_running_scans": "Scan in progress",
+ "label_status_paused_scans": "Scan paused",
+ "label_status_cell": "<0/> <1>{{count}}1>",
+ "label_status_tooltip": "{{count}}",
+ "label_status_tooltip_failed": "{{count}} Failed System",
+ "label_status_tooltip_failed_other": "{{count}} Failed Systems",
+ "label_status_tooltip_failed_sources": "{{count}} Failed Authentication",
+ "label_status_tooltip_failed_sources_other": "{{count}} Failed Authentications",
+ "label_status_tooltip_idCard": "{{count}} Credential",
+ "label_status_tooltip_idCard_other": "{{count}} Credentials",
+ "label_status_tooltip_scans": "{{count}} Previous scan",
+ "label_status_tooltip_scans_other": "{{count}} Previous scans",
+ "label_status_tooltip_sources": "{{count}} Source",
+ "label_status_tooltip_sources_other": "{{count}} Sources",
+ "label_status_tooltip_success": "{{count}} Successful System",
+ "label_status_tooltip_success_other": "{{count}} Successful Systems",
+ "label_status_tooltip_success_sources": "{{count}} Successful Authentication",
+ "label_status_tooltip_success_sources_other": "{{count}} Successful Authentications",
+ "label_status_tooltip_unreachable": "{{count}} Unreachable System",
+ "label_status_tooltip_unreachable_other": "{{count}} Unreachable Systems",
+ "tooltip_merge-reports": "Merge selected scan results into a single report"
+ },
+ "toast-notifications": {
+ "description": "{{context}}",
+ "description_add-source_hidden": "Source {{name}} was created",
+ "description_add-source_hidden_edit": "Source {{name}} was updated",
+ "description_add-source_hidden_error": "{{message}}",
+ "description_add-source_hidden_error_edit": "{{message}}",
+ "description_deleted-source": "Deleted source {{name}}.",
+ "description_error": "Application error",
+ "description_scan-report_canceled": "Scan <0>{{name}}0> stopped",
+ "description_scan-report_download": "Report <0>{{name}}0> downloaded",
+ "description_scan-report_paused": "Scan <0>{{name}}0> paused",
+ "description_scan-report_restart": "Scan <0>{{name}}0> resumed",
+ "description_scan-report_play": "Scan <0>{{name}}0> started",
+ "description_warning": "Application warning",
+ "title": "Notification",
+ "title_add-source_hidden": "Success creating source",
+ "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-source": "Success deleting source",
+ "title_error": "Error",
+ "title_warning": "Warning"
+ },
+ "view": {
+ "empty-state_description_credentials": "Credentials contain authentication information needed to scan a source. A credential includes a username and a password or SSH key. {{name}} uses SSH to connect to servers on the network and uses credentials to access those servers.",
+ "empty-state_description_scans": "Select a Source to scan from the Sources page.",
+ "empty-state_description_scans_zero": "$t(view.empty-state_description_sources)",
+ "empty-state_description_sources": "Begin by adding one or more sources. A source contains a collection of network information, including systems management solution information, IP addresses, or host names, in addition to SSH ports and credentials.",
+ "empty-state_filter_description": "The active filters are hiding all items.",
+ "empty-state_filter_title": "No Results Match the Filter Criteria",
+ "empty-state_label_clear": "Clear Filters",
+ "empty-state_label_sources": "Add Source",
+ "empty-state_label_source-navigate": "Go to Sources",
+ "empty-state_label_source-navigate_zero": "Add Source",
+ "empty-state_title": "Welcome to {{name}}",
+ "empty-state_title_scans": "No scans exist yet",
+ "empty-state_title_scans_zero": "Welcome to {{name}}",
+ "error_authentication": "Login error",
+ "error_credentials": "Credentials error",
+ "error_scan-hosts": "Scan hosts error",
+ "error_scan-jobs": "Scan jobs error",
+ "error_scans": "Scans error",
+ "error_sources": "Sources error",
+ "error-message_authentication": "{{message}} Please <0>login0> to continue.",
+ "error-message_credentials": "Error retrieving credentials: {{message}}",
+ "error-message_scan-hosts": "Error retrieving scan results: {{message}}",
+ "error-message_scan-jobs": "Error retrieving scan jobs: {{message}}",
+ "error-message_scans": "Error retrieving scans: {{message}}",
+ "error-message_sources": "Error retrieving sources: {{message}}",
+ "loading": "Loading...",
+ "loading_authentication": "Logging in...",
+ "loading_credentials": "Loading credentials..."
+ }
+}
diff --git a/scripts/api.sh b/scripts/api.sh
index ae50da21..5441cf15 100644
--- a/scripts/api.sh
+++ b/scripts/api.sh
@@ -87,7 +87,7 @@ startDB()
{
local CONTAINER="postgres:9.6.10"
local NAME=$1
- local DATA="$(pwd)/.container/postgres"
+ local DATA="$(pwd)/.container/${NAME}/postgres"
local DATA_VOLUME="/var/lib/postgresql/data"
local STORE_DATA=$2
@@ -110,8 +110,8 @@ startDB()
checkContainerRunning $NAME
- if [ ! -z "$(docker ps | grep $CONTAINER)" ]; then
- echo " Container: $(docker ps | grep $CONTAINER | cut -c 1-80)"
+ if [ ! -z "$(docker ps | grep $NAME)" ]; then
+ echo " Container: $(docker ps | grep $NAME | cut -c 1-80)"
echo " QPC DB running:"
printf " To stop: $ ${GREEN}docker stop ${NAME}${NOCOLOR}\n"
fi
@@ -148,7 +148,7 @@ buildApp()
reviewApi()
{
local NAME="qpc-review"
- local DB_NAME="qpc-db"
+ local DB_NAME="qpc-db-review"
local PORT=$1
local IS_BUILT=$2
local CONTAINER=$3
@@ -162,17 +162,17 @@ reviewApi()
startDB $DB_NAME
- if [ -z "$(docker ps | grep $CONTAINER)" ]; then
+ if [ -z "$(docker ps | grep $NAME)" ]; then
printf "\n"
echo "Starting API..."
docker run -d --rm -p $PORT:443 -e QPC_DBMS_HOST=$DB_NAME --link $DB_NAME:qpc-link --name $NAME $CONTAINER >/dev/null
fi
- checkContainerRunning $CONTAINER
+ checkContainerRunning $NAME
- if [ ! -z "$(docker ps | grep $CONTAINER)" ]; then
- echo " Container: $(docker ps | grep $CONTAINER | cut -c 1-80)"
- echo " QPC API running: https://localhost:${PORT}/"
+ if [ ! -z "$(docker ps | grep $NAME)" ]; then
+ echo " Container: $(docker ps | grep $NAME | cut -c 1-80)"
+ echo " QPC API running: https://127.0.0.1:${PORT}/"
printf " To stop: $ ${GREEN}docker stop ${NAME}${NOCOLOR}\n"
fi
@@ -185,7 +185,7 @@ reviewApi()
stageApi()
{
local NAME="qpc-stage"
- local DB_NAME="qpc-db"
+ local DB_NAME="qpc-db-stage"
local PORT=$1
local UPDATE=$2
local IS_BUILT=$3
@@ -209,23 +209,22 @@ stageApi()
fi
if [ ! "$UPDATE" = true ]; then
- startDB $DB_NAME true
+ startDB $DB_NAME
- if [ -z "$(docker ps | grep $CONTAINER)" ]; then
+ if [ -z "$(docker ps | grep $NAME)" ]; then
printf "\n"
echo "Starting API..."
docker run -d --rm -p $PORT:443 -v $BUILD_DIR:$CLIENT_VOLUME -v $BUILD_DIR:$TEMPLATE_CLIENT_VOLUME -v $TEMPLATE_DIR:$TEMPLATE_REGISTRATION_VOLUME -e QPC_DBMS_HOST=$DB_NAME --link $DB_NAME:qpc-link --name $NAME $CONTAINER >/dev/null
fi
- checkContainerRunning $CONTAINER
+ checkContainerRunning $NAME
- if [ ! -z "$(docker ps | grep $CONTAINER)" ]; then
- echo " Container: $(docker ps | grep $CONTAINER | cut -c 1-80)"
+ if [ ! -z "$(docker ps | grep $NAME)" ]; then
+ echo " Container: $(docker ps | grep $NAME | cut -c 1-80)"
echo " QPC API running: https://localhost:${PORT}/"
printf " To stop: $ ${GREEN}docker stop ${NAME}${NOCOLOR}\n"
fi
- runDocs
exit 0
fi
}
@@ -264,7 +263,6 @@ devApi()
printf " To stop: $ ${GREEN}docker stop ${NAME}${NOCOLOR}\n"
fi
- runDocs
exit 0
fi
}
@@ -272,7 +270,7 @@ devApi()
#
# Serve swagger spec
#
-runDocs()
+runSpecs()
{
local GITREPO=$1
@@ -284,6 +282,14 @@ runDocs()
}
#
#
+# Stop QPC containers
+#
+stopApi()
+{
+ docker stop $(docker ps --filter name="qpc*")
+}
+#
+#
# main()
#
{
@@ -340,10 +346,12 @@ runDocs()
stageApi $PORT $UPDATE $BUILT $QPC_IMAGE_CONTAINER;;
dev )
devApi $PORT "$FILE" $UPDATE $MOCK_IMAGE_CONTAINER;;
- docs )
- runDocs true;;
+ specs )
+ runSpecs true;;
gitApi )
gitApi;;
+ stopApi )
+ stopApi;;
update )
devApi $PORT "$FILE" true
stageApi $PORT true
diff --git a/src/common/__tests__/__snapshots__/helpers.test.js.snap b/src/common/__tests__/__snapshots__/helpers.test.js.snap
index c7f01dd7..01f409df 100644
--- a/src/common/__tests__/__snapshots__/helpers.test.js.snap
+++ b/src/common/__tests__/__snapshots__/helpers.test.js.snap
@@ -52,6 +52,18 @@ exports[`Helpers should handle http status less than 400 message from response:
exports[`Helpers should handle http status less than 400 message from response: 400 message 1`] = `"Request success XXX"`;
+exports[`Helpers should handle use aggregate error, or fallback: emulated aggregate error 1`] = `
+Object {
+ "aggregated": [AggregateError: testing aggregated],
+ "errors": Array [
+ [Error: lorem ipsum],
+ [Error: dolor sit],
+ ],
+ "isEmulated": true,
+ "name": "AggregateError",
+}
+`;
+
exports[`Helpers should handle view related selectors and props updates: createViewQueryObject 1`] = `
Object {
"page": 1,
@@ -62,8 +74,10 @@ Object {
exports[`Helpers should have specific functions: helpers 1`] = `
Object {
"DEV_MODE": false,
+ "POLL_INTERVAL": 120000,
"PROD_MODE": false,
"TEST_MODE": true,
+ "TOAST_NOTIFICATIONS_TIMEOUT": 8000,
"UI_BRAND": false,
"UI_NAME": "Quipucords",
"UI_SENTENCE_START_NAME": "The Quipucords tool",
@@ -75,6 +89,7 @@ Object {
"devModeNormalizeCount": [Function],
"downloadData": [Function],
"generateId": [Function],
+ "getCurrentDate": [Function],
"getMessageFromResults": [Function],
"getStatusFromResults": [Function],
"getTimeDisplayHowLongAgo": [Function],
@@ -93,6 +108,12 @@ Object {
}
`;
+exports[`Helpers should return a predictable current date: current date 1`] = `
+Object {
+ "currentDate": 2022-06-01T00:00:00.000Z,
+}
+`;
+
exports[`Helpers should support displaying the ui version: ui version 1`] = `"0.0.0.0000000"`;
exports[`Helpers should support icon references: scanStatusIcon 1`] = `
diff --git a/src/common/__tests__/helpers.test.js b/src/common/__tests__/helpers.test.js
index b9080d51..9cbe77cf 100644
--- a/src/common/__tests__/helpers.test.js
+++ b/src/common/__tests__/helpers.test.js
@@ -5,6 +5,21 @@ describe('Helpers', () => {
expect(helpers).toMatchSnapshot('helpers');
});
+ it('should handle use aggregate error, or fallback', () => {
+ const aggregateError = window.AggregateError;
+ window.AggregateError = undefined;
+ const aggregated = helpers.aggregatedError(
+ [new Error('lorem ipsum'), new Error('dolor sit')],
+ 'testing aggregated'
+ );
+ expect({
+ aggregated,
+ ...aggregated
+ }).toMatchSnapshot('emulated aggregate error');
+
+ window.AggregateError = aggregateError;
+ });
+
it('should support generated strings and flags', () => {
expect(helpers.generateId()).toBe('generatedid-');
expect(helpers.generateId('lorem')).toBe('lorem-');
@@ -175,4 +190,9 @@ describe('Helpers', () => {
expect(helpers.ipAddressValue('0.0.0.1.5')).toBe(1);
expect(Number.isNaN(helpers.ipAddressValue('lorem'))).toBe(true);
});
+
+ it('should return a predictable current date', () => {
+ const currentDate = helpers.getCurrentDate();
+ expect({ currentDate }).toMatchSnapshot('current date');
+ });
});
diff --git a/src/common/helpers.js b/src/common/helpers.js
index 7ba541d3..633996c4 100644
--- a/src/common/helpers.js
+++ b/src/common/helpers.js
@@ -421,7 +421,7 @@ const getStatusFromResults = results => {
/**
* Return a callback for determining a timestamp.
*
- * @returns {Function}
+ * @type {Function}
*/
const getTimeStampFromResults =
process.env.REACT_APP_ENV !== 'test'
@@ -431,7 +431,7 @@ const getTimeStampFromResults =
/**
* Return a callback for determine time offset.
*
- * @returns {Function}
+ * @type {Function}
*/
const getTimeDisplayHowLongAgo =
process.env.REACT_APP_ENV !== 'test'
@@ -496,12 +496,12 @@ const TEST_MODE = process.env.REACT_APP_ENV === 'test';
const UI_BRAND = process.env.REACT_APP_UI_BRAND === 'true';
/**
- * UI coded name.
+ * UI coded name, brand dependent.
* See dotenv config files for updating.
*
* @type {string}
*/
-const UI_NAME = process.env.REACT_APP_UI_NAME;
+const UI_NAME = UI_BRAND === true ? process.env.REACT_APP_UI_BRAND_NAME : process.env.REACT_APP_UI_NAME;
/**
* UI cased sentence start name.
@@ -509,7 +509,8 @@ const UI_NAME = process.env.REACT_APP_UI_NAME;
*
* @type {string}
*/
-const UI_SENTENCE_START_NAME = process.env.REACT_APP_UI_SENTENCE_START_NAME;
+const UI_SENTENCE_START_NAME =
+ UI_BRAND === true ? process.env.REACT_APP_UI_BRAND_SENTENCE_START_NAME : process.env.REACT_APP_UI_SENTENCE_START_NAME;
/**
* UI short name.
@@ -517,7 +518,8 @@ const UI_SENTENCE_START_NAME = process.env.REACT_APP_UI_SENTENCE_START_NAME;
*
* @type {string}
*/
-const UI_SHORT_NAME = process.env.REACT_APP_UI_SHORT_NAME;
+const UI_SHORT_NAME =
+ UI_BRAND === true ? process.env.REACT_APP_UI_BRAND_SHORT_NAME : process.env.REACT_APP_UI_SHORT_NAME;
/**
* UI packaged application version, with generated hash.
@@ -527,6 +529,27 @@ const UI_SHORT_NAME = process.env.REACT_APP_UI_SHORT_NAME;
*/
const UI_VERSION = process.env.REACT_APP_UI_VERSION;
+/**
+ * Timeout for toast alert notifications.
+ *
+ * @type {number|undefined}
+ */
+const TOAST_NOTIFICATIONS_TIMEOUT = Number.parseInt(process.env.REACT_APP_TOAST_NOTIFICATIONS_TIMEOUT, 10) || undefined;
+
+/**
+ * Global poll interval
+ *
+ * @type {number|undefined}
+ */
+const POLL_INTERVAL = Number.parseInt(process.env.REACT_APP_POLL_INTERVAL, 10) || undefined;
+
+/**
+ * Return a consistent current date.
+ *
+ * @returns {string|Date}
+ */
+const getCurrentDate = () => (TEST_MODE && moment.utc('20220601').toDate()) || moment.utc().toDate();
+
const helpers = {
aggregatedError,
copyClipboard,
@@ -550,13 +573,16 @@ const helpers = {
isIpAddress,
ipAddressValue,
DEV_MODE,
+ POLL_INTERVAL,
PROD_MODE,
TEST_MODE,
+ TOAST_NOTIFICATIONS_TIMEOUT,
UI_BRAND,
UI_NAME,
UI_SENTENCE_START_NAME,
UI_SHORT_NAME,
- UI_VERSION
+ UI_VERSION,
+ getCurrentDate
};
export { helpers as default, helpers };
diff --git a/src/components/aboutModal/__tests__/__snapshots__/aboutModal.test.js.snap b/src/components/aboutModal/__tests__/__snapshots__/aboutModal.test.js.snap
index 5e36f0e9..091e82c9 100644
--- a/src/components/aboutModal/__tests__/__snapshots__/aboutModal.test.js.snap
+++ b/src/components/aboutModal/__tests__/__snapshots__/aboutModal.test.js.snap
@@ -2,50 +2,58 @@
exports[`AboutModal Component should contain brand: brand 1`] = `
}
- show={true}
- trademarkText="Copyright (c) 2019 - 2022 Red Hat Inc."
+ trademark="t(about-modal.copyright, {\\"year\\":\\"2022\\"})"
>
-
-
-
+
+
+
+ t(about-modal.ui-version)
+
+
+ 0.0.0.0000000
+
+
+
-
@@ -55,7 +63,7 @@ exports[`AboutModal Component should contain brand: brand 1`] = `
exports[`AboutModal Component should have a copy event that updates state: post copy event 1`] = `
Object {
"copied": null,
- "timer": 14,
+ "timer": 13,
}
`;
@@ -71,7 +79,6 @@ exports[`AboutModal Component should render a connected component with default p
exports[`AboutModal Component should render a non-connected component: hidden modal 1`] = `
}
- show={false}
- trademarkText=""
+ trademark=""
>
- }
>
-
}
- onEntering={[Function]}
- onExited={[Function]}
- onHide={[Function]}
- onMouseUp={[Function]}
- renderBackdrop={[Function]}
- restoreFocus={true}
- show={false}
- transition={[Function]}
+ trademark=""
/>
-
+
`;
diff --git a/src/components/aboutModal/aboutModal.js b/src/components/aboutModal/aboutModal.js
index a48e37cd..8e88cd47 100644
--- a/src/components/aboutModal/aboutModal.js
+++ b/src/components/aboutModal/aboutModal.js
@@ -1,14 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
+import moment from 'moment';
import { detect } from 'detect-browser';
-import { AboutModal as PfAboutModal, Button, Icon } from 'patternfly-react';
-import { connectTranslate, reduxActions, reduxTypes, store } from '../../redux';
-import helpers from '../../common/helpers';
+import {
+ AboutModal as PfAboutModal,
+ Button,
+ ButtonVariant,
+ TextContent,
+ TextList,
+ TextListItem
+} from '@patternfly/react-core';
+import { CheckIcon, PasteIcon } from '@patternfly/react-icons';
+import { connect, reduxActions, reduxTypes, store } from '../../redux';
+import { helpers } from '../../common';
+import { translate } from '../i18n/i18n';
import logoImg from '../../styles/images/logo.svg';
import titleImg from '../../styles/images/title.svg';
import logoImgBrand from '../../styles/images/logo-brand.svg';
import titleImgBrand from '../../styles/images/title-brand.svg';
+/**
+ * About modal, display application information.
+ */
class AboutModal extends React.Component {
selectElement = React.createRef();
@@ -66,6 +79,7 @@ class AboutModal extends React.Component {
const { copied } = this.state;
const { show, serverVersion, t, uiBrand, uiName, uiShortName, uiVersion, username } = this.props;
const browser = detect();
+ const currentYear = moment.utc(helpers.getCurrentDate()).format('YYYY');
const props = {
show,
@@ -78,47 +92,69 @@ class AboutModal extends React.Component {
if (uiBrand) {
props.logo = logoImgBrand;
props.productTitle = ;
- props.trademarkText = 'Copyright (c) 2019 - 2022 Red Hat Inc.';
+ props.trademarkText = t('about-modal.copyright', { year: currentYear });
}
return (
-
-
-
- {username && (
-
- )}
- {browser && (
-
- )}
- {browser && (
-
- )}
- {serverVersion && (
-
- )}
- {uiVersion && (
-
- )}
-
+
+
+
+
+ {username && (
+
+ {t('about-modal.username')}
+ {username || ''}
+
+ )}
+ {browser && (
+
+ {t('about-modal.browser-version')}
+ {`${browser.name} ${browser.version}`}
+
+ )}
+ {browser && (
+
+ {t('about-modal.browser-os')}
+ {browser.os || ''}
+
+ )}
+ {serverVersion && (
+
+ {t('about-modal.server-version')}
+ {serverVersion}
+
+ )}
+ {uiVersion && (
+
+ {t('about-modal.ui-version')}
+ {uiVersion}
+
+ )}
+
+
- {copied && }
- {!copied && }
+ {(copied && ) || }
@@ -126,9 +162,14 @@ class AboutModal extends React.Component {
}
}
+/**
+ * Prop types.
+ *
+ * @type {{uiShortName: string, serverVersion: string, t: Function, resetTimer: number, show: boolean,
+ * uiVersion: string, getStatus: Function, uiName: string, uiBrand: boolean, username: string}}
+ */
AboutModal.propTypes = {
getStatus: PropTypes.func,
- getUser: PropTypes.func,
resetTimer: PropTypes.number,
serverVersion: PropTypes.string,
show: PropTypes.bool.isRequired,
@@ -140,12 +181,17 @@ AboutModal.propTypes = {
username: PropTypes.string
};
+/**
+ * Default props.
+ *
+ * @type {{uiShortName: string, serverVersion: null, t: translate, resetTimer: number, uiVersion: string,
+ * getStatus: Function, uiName: string, uiBrand: boolean, username: null}}
+ */
AboutModal.defaultProps = {
getStatus: helpers.noop,
- getUser: helpers.noop,
resetTimer: 3000,
serverVersion: null,
- t: helpers.noopTranslate,
+ t: translate,
uiBrand: helpers.UI_BRAND,
uiName: helpers.UI_NAME,
uiShortName: helpers.UI_SHORT_NAME,
@@ -163,6 +209,6 @@ const mapStateToProps = state => ({
username: state.user.session.username
});
-const ConnectedAboutModal = connectTranslate(mapStateToProps, mapDispatchToProps)(AboutModal);
+const ConnectedAboutModal = connect(mapStateToProps, mapDispatchToProps)(AboutModal);
export { ConnectedAboutModal as default, ConnectedAboutModal, AboutModal };
diff --git a/src/components/addCredentialType/__tests__/__snapshots__/addCredentialType.test.js.snap b/src/components/addCredentialType/__tests__/__snapshots__/addCredentialType.test.js.snap
new file mode 100644
index 00000000..3155d747
--- /dev/null
+++ b/src/components/addCredentialType/__tests__/__snapshots__/addCredentialType.test.js.snap
@@ -0,0 +1,70 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ToolbarFieldGranularity Component should export select options: fieldOptions 1`] = `
+Array [
+ Object {
+ "title": [Function],
+ "value": "network",
+ },
+ Object {
+ "title": [Function],
+ "value": "satellite",
+ },
+ Object {
+ "title": [Function],
+ "value": "vcenter",
+ },
+]
+`;
+
+exports[`ToolbarFieldGranularity Component should handle opening the credential dialog through redux state with hook: dispatch, hook 1`] = `
+Array [
+ Array [
+ Array [
+ Object {
+ "credentialType": "dolor sit",
+ "type": "CREATE_CREDENTIAL_SHOW",
+ },
+ ],
+ ],
+]
+`;
+
+exports[`ToolbarFieldGranularity Component should render a basic component: basic 1`] = `
+
+`;
diff --git a/src/components/addCredentialType/__tests__/addCredentialType.test.js b/src/components/addCredentialType/__tests__/addCredentialType.test.js
new file mode 100644
index 00000000..029f1a1c
--- /dev/null
+++ b/src/components/addCredentialType/__tests__/addCredentialType.test.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { AddCredentialType, fieldOptions, useOnSelect } from '../addCredentialType';
+import { store } from '../../../redux/store';
+
+describe('ToolbarFieldGranularity Component', () => {
+ let mockDispatch;
+
+ beforeEach(() => {
+ mockDispatch = jest.spyOn(store, 'dispatch').mockImplementation((type, data) => ({ type, data }));
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render a basic component', () => {
+ const props = {};
+ const component = shallow(
);
+
+ expect(component).toMatchSnapshot('basic');
+ });
+
+ it('should export select options', () => {
+ expect(fieldOptions).toMatchSnapshot('fieldOptions');
+ });
+
+ it('should handle opening the credential dialog through redux state with hook', () => {
+ const options = {};
+ const onSelect = useOnSelect(options);
+
+ onSelect({ value: 'dolor sit' });
+ expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch, hook');
+ });
+});
diff --git a/src/components/addCredentialType/addCredentialType.js b/src/components/addCredentialType/addCredentialType.js
new file mode 100644
index 00000000..8ef8f84e
--- /dev/null
+++ b/src/components/addCredentialType/addCredentialType.js
@@ -0,0 +1,94 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { DropdownSelect, ButtonVariant, SelectPosition } from '../dropdownSelect/dropdownSelect';
+import { reduxTypes, storeHooks } from '../../redux';
+import { translate } from '../i18n/i18n';
+
+/**
+ * Select field options.
+ *
+ * @type {{title: Function|string, value: string}[]}
+ */
+const fieldOptions = [
+ { title: () => translate('form-dialog.label', { context: ['option', 'network'] }), value: 'network' },
+ { title: () => translate('form-dialog.label', { context: ['option', 'satellite'] }), value: 'satellite' },
+ { title: () => translate('form-dialog.label', { context: ['option', 'vcenter'] }), value: 'vcenter' }
+];
+
+/**
+ * On select create credential
+ *
+ * @param {object} options
+ * @param {Function} options.useDispatch
+ * @returns {Function}
+ */
+const useOnSelect = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => {
+ const dispatch = useAliasDispatch();
+
+ return ({ value = null }) => {
+ dispatch([
+ {
+ type: reduxTypes.credentials.CREATE_CREDENTIAL_SHOW,
+ credentialType: value
+ }
+ ]);
+ };
+};
+
+/**
+ * Display available credential types.
+ *
+ * @param {object} props
+ * @param {Array} props.options
+ * @param {string} props.placeholder
+ * @param {Function} props.t
+ * @param {Function} props.useOnSelect
+ * @param {object} props.props
+ * @returns {React.ReactNode}
+ */
+const AddCredentialType = ({ options, placeholder, t, useOnSelect: useAliasOnSelect, ...props } = {}) => {
+ const onSelect = useAliasOnSelect();
+
+ return (
+
+ );
+};
+
+/**
+ * Prop types
+ *
+ * @type {{useOnSelect: Function, options: Array, placeholder: string}}
+ */
+AddCredentialType.propTypes = {
+ options: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
+ value: PropTypes.any,
+ selected: PropTypes.bool
+ })
+ ),
+ placeholder: PropTypes.string,
+ t: PropTypes.func,
+ useOnSelect: PropTypes.func
+};
+
+/**
+ * Default props
+ *
+ * @type {{useOnSelect: Function, t: translate, options: {title: (Function|string), value: string}[],
+ * placeholder: null}}
+ */
+AddCredentialType.defaultProps = {
+ options: fieldOptions,
+ placeholder: null,
+ t: translate,
+ useOnSelect
+};
+
+export { AddCredentialType as default, AddCredentialType, fieldOptions, useOnSelect, ButtonVariant, SelectPosition };
diff --git a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizard.test.js.snap b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizard.test.js.snap
index ab5fa92e..db1356ca 100644
--- a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizard.test.js.snap
+++ b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizard.test.js.snap
@@ -1,121 +1,131 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`AddSourceWizard Component should allow cancelling the wizard: cancel 1`] = `
+Array [
+ Array [
+ Object {
+ "source": Object {
+ "source_type": "network",
+ },
+ "type": "VALID_SOURCE_WIZARD_STEPONE",
+ },
+ ],
+ Array [
+ Object {
+ "body": "t(form-dialog.confirmation_body_add-source, {\\"context\\":\\" \\"})",
+ "cancelButtonText": "t(form-dialog.label, {\\"context\\":\\"no\\"})",
+ "confirmButtonText": "t(form-dialog.label, {\\"context\\":\\"yes\\"})",
+ "heading": "t(form-dialog.confirmation_heading_add-source, {\\"context\\":\\"\\"})",
+ "onConfirm": [Function],
+ "title": "t(form-dialog.confirmation_title_add-source, {\\"context\\":\\"\\"})",
+ "type": "CONFIRMATION_MODAL_SHOW",
+ "variant": "medium",
+ },
+ ],
+]
+`;
+
+exports[`AddSourceWizard Component should allow closing the wizard on fulfillment: fulfill 1`] = `
+Array [
+ Array [
+ Object {
+ "source": Object {
+ "source_type": "network",
+ },
+ "type": "VALID_SOURCE_WIZARD_STEPONE",
+ },
+ ],
+ Array [
+ Array [
+ Object {
+ "type": "CONFIRMATION_MODAL_HIDE",
+ },
+ Object {
+ "type": "UPDATE_SOURCE_HIDE",
+ },
+ ],
+ ],
+ Array [
+ Object {
+ "type": "UPDATE_SOURCES",
+ },
+ ],
+]
+`;
+
exports[`AddSourceWizard Component should display update steps: update 1`] = `
-
-
- ,
- ,
- ]
- }
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
- Cancel
-
-
-
- Back
-
-
- Save
-
-
-
-
+ onNext={[Function]}
+ steps={
+ Array [
+ Object {
+ "canJumpTo": false,
+ "component":
,
+ "enableNext": false,
+ "id": 1,
+ "name": "t(form-dialog.title_add-source_step, {\\"context\\":\\"two\\"})",
+ "nextButtonText": "t(form-dialog.label_submit, {\\"context\\":\\"add-source\\"})",
+ },
+ Object {
+ "canJumpTo": false,
+ "component":
,
+ "enableNext": false,
+ "hideBackButton": false,
+ "hideCancelButton": false,
+ "id": 2,
+ "name": "t(form-dialog.title_add-source_step, {\\"context\\":\\"three\\"})",
+ "nextButtonText": "t(form-dialog.label, {\\"context\\":\\"close\\"})",
+ },
+ ]
+ }
+ title="t(form-dialog.title_add-source, {\\"context\\":\\"edit\\"})"
+ variant="medium"
+/>
`;
-exports[`AddSourceWizard Component should not display a wizard 1`] = `null`;
-
-exports[`AddSourceWizard Component should render a connected component: connected 1`] = `
`;
+exports[`AddSourceWizard Component should render a basic component: basic 1`] = `
+
,
+ "enableNext": false,
+ "id": 1,
+ "name": "t(form-dialog.title_add-source, {\\"context\\":\\"step\\"})",
+ },
+ Object {
+ "canJumpTo": false,
+ "component":
,
+ "enableNext": false,
+ "id": 2,
+ "name": "t(form-dialog.title_add-source_step, {\\"context\\":\\"two\\"})",
+ "nextButtonText": "t(form-dialog.label_submit, {\\"context\\":\\"add-source\\"})",
+ },
+ Object {
+ "canJumpTo": false,
+ "component":
,
+ "enableNext": false,
+ "hideBackButton": false,
+ "hideCancelButton": false,
+ "id": 3,
+ "name": "t(form-dialog.title_add-source_step, {\\"context\\":\\"three\\"})",
+ "nextButtonText": "t(form-dialog.label, {\\"context\\":\\"close\\"})",
+ },
+ ]
+ }
+ title="t(form-dialog.title, {\\"context\\":\\"add-source\\"})"
+ variant="medium"
+/>
+`;
diff --git a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardContext.test.js.snap b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardContext.test.js.snap
new file mode 100644
index 00000000..82bc6879
--- /dev/null
+++ b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardContext.test.js.snap
@@ -0,0 +1,74 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AddSourceWizardContext should apply a hook for retrieving data from a selector: dispatch confirmations on hide wizard 1`] = `
+Array [
+ Array [
+ Object {
+ "alertType": "danger",
+ "header": "t(toast-notifications.title_add-source_hidden, {\\"context\\":\\"error\\"})",
+ "message": "t(toast-notifications.description_add-source_hidden, {\\"context\\":\\"error\\"})",
+ "type": "TOAST_ADD",
+ },
+ ],
+ Array [
+ Object {
+ "alertType": "success",
+ "header": "t(toast-notifications.title_add-source_hidden, {\\"context\\":\\"\\"})",
+ "message": "t(toast-notifications.description_add-source_hidden, {\\"context\\":\\"\\",\\"name\\":\\"lorem ipsum\\"})",
+ "type": "TOAST_ADD",
+ },
+ ],
+]
+`;
+
+exports[`AddSourceWizardContext should apply a hook for retrieving data from a selector: error response 1`] = `
+Object {
+ "data": Object {
+ "messages": Object {},
+ },
+ "error": true,
+ "show": false,
+}
+`;
+
+exports[`AddSourceWizardContext should apply a hook for retrieving data from a selector: error show response 1`] = `
+Object {
+ "data": Object {
+ "messages": Object {},
+ },
+ "error": true,
+}
+`;
+
+exports[`AddSourceWizardContext should apply a hook for retrieving data from a selector: success response 1`] = `
+Object {
+ "fulfilled": true,
+}
+`;
+
+exports[`AddSourceWizardContext should apply a hook for retrieving data from a selector: success show response 1`] = `
+Object {
+ "fulfilled": true,
+ "show": false,
+ "source": Object {
+ "name": "lorem ipsum",
+ },
+}
+`;
+
+exports[`AddSourceWizardContext should handle an onShowAddSourceWizard event: onColumnSort event, dispatch 1`] = `
+Array [
+ Array [
+ Object {
+ "type": "CREATE_SOURCE_SHOW",
+ },
+ ],
+]
+`;
+
+exports[`AddSourceWizardContext should return specific properties: specific properties 1`] = `
+Object {
+ "useGetAddSource": [Function],
+ "useOnShowAddSourceWizard": [Function],
+}
+`;
diff --git a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepOne.test.js.snap b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepOne.test.js.snap
index cb48a2b6..d8bc8386 100644
--- a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepOne.test.js.snap
+++ b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepOne.test.js.snap
@@ -6,56 +6,65 @@ exports[`AddSourceWizardStepOne Component should render a non-connected componen
diff --git a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepThree.test.js.snap b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepThree.test.js.snap
index 61619fd8..e2e106a2 100644
--- a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepThree.test.js.snap
+++ b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepThree.test.js.snap
@@ -10,41 +10,71 @@ exports[`AccountWizardStepResults Component should render a wizard results step
fulfilled={false}
name={null}
pending={false}
+ t={[Function]}
>
-
-
-
-
-
-
+ className="pf-c-empty-state__icon"
+ color="#c9190b"
+ noVerticalAlign={false}
+ size="sm"
+ >
+
+
+
+
+
+
+
+ t(form-dialog.empty-state_title_add-source, {"context":"error","name":null})
+
+
+
+
+ t(form-dialog.empty-state_description_add-source_error, {"context":" ","name":null})
+
+
+
-
- Error
- Creating
- Source
-
-
- There are errors on a previous step. Use the Back button to review your settings and try again.
-
-
+
`;
@@ -56,41 +86,71 @@ exports[`AccountWizardStepResults Component should render a wizard results step
fulfilled={false}
name={null}
pending={false}
+ t={[Function]}
>
-
-
-
-
-
-
+ className="pf-c-empty-state__icon"
+ color="#c9190b"
+ noVerticalAlign={false}
+ size="sm"
+ >
+
+
+
+
+
+
+
+ t(form-dialog.empty-state_title_add-source_error, {"context":"edit","name":null})
+
+
+
+
+ t(form-dialog.empty-state_description_add-source_error, {"context":" ","name":null})
+
+
+
-
- Error
- Updating
- Source
-
-
- There are errors on a previous step. Use the Back button to review your settings and try again.
-
-
+
`;
@@ -101,39 +161,71 @@ exports[`AccountWizardStepResults Component should render a wizard results step
fulfilled={true}
name="Dolor"
pending={false}
+ t={[Function]}
>
-
-
-
-
-
-
+ className="pf-c-empty-state__icon"
+ color="#3e8635"
+ noVerticalAlign={false}
+ size="sm"
+ >
+
+
+
+
+
+
+
+ t(form-dialog.empty-state_title_add-source, {"context":"","name":"Dolor"})
+
+
+
+
+ t(form-dialog.empty-state_description_add-source, {"context":" ","name":"Dolor"})
+
+
+
-
-
- Dolor
-
- was
- created
- .
-
-
+
`;
@@ -144,39 +236,71 @@ exports[`AccountWizardStepResults Component should render a wizard results step
fulfilled={true}
name="Dolor"
pending={false}
+ t={[Function]}
>
-
-
-
-
-
-
+ className="pf-c-empty-state__icon"
+ color="#3e8635"
+ noVerticalAlign={false}
+ size="sm"
+ >
+
+
+
+
+
+
+
+ t(form-dialog.empty-state_title_add-source, {"context":"edit","name":"Dolor"})
+
+
+
+
+ t(form-dialog.empty-state_description_add-source, {"context":" ","name":"Dolor"})
+
+
+
-
-
- Dolor
-
- was
- updated
- .
-
-
+
`;
@@ -187,39 +311,65 @@ exports[`AccountWizardStepResults Component should render a wizard results step
fulfilled={false}
name="Dolor"
pending={true}
+ t={[Function]}
>
-
-
-
-
- Creating
- Source...
-
-
- Please wait while source
-
- Dolor
-
- is being
- created
- .
-
-
+ className="pf-c-empty-state__content"
+ >
+
+
+
+
+
+
+
+
+
+
+
+ t(form-dialog.empty-state_title_add-source, {"context":"pending","name":"Dolor"})
+
+
+
+
+ t(form-dialog.empty-state_description_add-source_pending, {"context":" ","name":"Dolor"})
+
+
+
+
+
`;
@@ -230,38 +380,64 @@ exports[`AccountWizardStepResults Component should render a wizard results step
fulfilled={false}
name="Dolor"
pending={true}
+ t={[Function]}
>
-
-
-
-
- Updating
- Source...
-
-
- Please wait while source
-
- Dolor
-
- is being
- updated
- .
-
-
+ className="pf-c-empty-state__content"
+ >
+
+
+
+
+
+
+
+
+
+
+
+ t(form-dialog.empty-state_title_add-source_pending, {"context":"edit","name":"Dolor"})
+
+
+
+
+ t(form-dialog.empty-state_description_add-source_pending, {"context":"edit","name":"Dolor"})
+
+
+
+
+
`;
diff --git a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepTwo.test.js.snap b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepTwo.test.js.snap
index 0c74553a..57df83e2 100644
--- a/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepTwo.test.js.snap
+++ b/src/components/addSourceWizard/__tests__/__snapshots__/addSourceWizardStepTwo.test.js.snap
@@ -109,49 +109,82 @@ exports[`AddSourceWizardStepTwo Component should display different forms for sou
class="input-group"
>
-
-
- Add one or more credentials
-
-
-
-
-
+
+
+
+ t(form-dialog.label_placeholder_add-source_credential_multi, {"context":"add"})
+
+
+
+
+
+
+
+
+
- Add
+
+
+
-
@@ -251,49 +284,83 @@ exports[`AddSourceWizardStepTwo Component should display different forms for sou
class="input-group"
>
-
-
- Add a credential
-
-
-
-
-
+
+
+
+ t(form-dialog.label_placeholder_add-source_credential, {"context":"add"})
+
+
+
+
+
+
+
+
+
- Add
+
+
+
-
@@ -312,91 +379,51 @@ exports[`AddSourceWizardStepTwo Component should display different forms for sou
class="col-sm-9"
>
-
-
- SSLv23
-
-
-
-
-
+
- Disable SSL
-
-
-
+
+
+
+
+
+
@@ -425,6 +452,31 @@ exports[`AddSourceWizardStepTwo Component should display different forms for sou
`;
+exports[`AddSourceWizardStepTwo Component should export select options: options 1`] = `
+Array [
+ Object {
+ "title": "t(form-dialog.label_option, {\\"context\\":\\"SSLv23\\"})",
+ "value": "SSLv23",
+ },
+ Object {
+ "title": "t(form-dialog.label_option, {\\"context\\":\\"TLSv1\\"})",
+ "value": "TLSv1",
+ },
+ Object {
+ "title": "t(form-dialog.label_option, {\\"context\\":\\"TLSv1_1\\"})",
+ "value": "TLSv1_1",
+ },
+ Object {
+ "title": "t(form-dialog.label_option, {\\"context\\":\\"TLSv1_2\\"})",
+ "value": "TLSv1_2",
+ },
+ Object {
+ "title": "t(form-dialog.label_option, {\\"context\\":\\"disableSsl\\"})",
+ "value": "disableSsl",
+ },
+]
+`;
+
exports[`AddSourceWizardStepTwo Component should render a non-connected component: non-connected 1`] = `
)}
diff --git a/src/components/addSourceWizard/addSourceWizardStepThree.js b/src/components/addSourceWizard/addSourceWizardStepThree.js
index a1b2154f..057096d4 100644
--- a/src/components/addSourceWizard/addSourceWizardStepThree.js
+++ b/src/components/addSourceWizard/addSourceWizardStepThree.js
@@ -1,57 +1,71 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Icon, Spinner } from 'patternfly-react';
+import { EmptyState, EmptyStateBody, EmptyStateIcon, Spinner, Title } from '@patternfly/react-core';
+import { OutlinedCheckCircleIcon, ExclamationCircleIcon } from '@patternfly/react-icons';
+import { global_success_color_100 as green, global_danger_color_100 as red } from '@patternfly/react-tokens';
import { connect, reduxSelectors } from '../../redux';
+import { EMPTY_CONTEXT, translate } from '../i18n/i18n';
-const AddSourceWizardStepThree = ({ add, error, fulfilled, pending, name }) => (
+/**
+ * Add source wizard, step three, confirmation.
+ *
+ * @param {object} props
+ * @param {boolean} props.add
+ * @param {boolean} props.error
+ * @param {boolean} props.fulfilled
+ * @param {boolean} props.pending
+ * @param {string} props.name
+ * @param {Function} props.t
+ * @returns {React.ReactNode}
+ */
+const AddSourceWizardStepThree = ({ add, error, fulfilled, pending, name, t }) => (
- {error && (
-
-
-
-
-
Error {add ? 'Creating' : 'Updating'} Source
-
- There are errors on a previous step. Use the Back button to review your settings and try again.
-
-
- )}
- {fulfilled && (
-
-
-
-
-
- {name} was {add ? 'created' : 'updated'}.
-
-
- )}
- {pending && (
-
-
-
{add ? 'Creating' : 'Updating'} Source...
-
- Please wait while source {name} is being {add ? 'created' : 'updated'}.
-
-
- )}
+
+ {error && }
+ {fulfilled && }
+ {pending && }
+
+ {t('form-dialog.empty-state_title_add-source', {
+ context: [(error && 'error') || (pending && 'pending'), !add && 'edit'],
+ name
+ })}
+
+
+ {t(`form-dialog.empty-state_description_add-source`, {
+ context: [(error && 'error') || (pending && 'pending'), (!add && pending && 'edit') || EMPTY_CONTEXT],
+ name
+ })}
+
+
);
+/**
+ * Prop types
+ *
+ * @type {{add: boolean, t: Function, pending: boolean, fulfilled: boolean, name: string, error: boolean}}
+ */
AddSourceWizardStepThree.propTypes = {
add: PropTypes.bool,
error: PropTypes.bool,
fulfilled: PropTypes.bool,
pending: PropTypes.bool,
- name: PropTypes.string
+ name: PropTypes.string,
+ t: PropTypes.func
};
+/**
+ * Default props
+ *
+ * @type {{add: boolean, t: translate, pending: boolean, fulfilled: boolean, name: null, error: boolean}}
+ */
AddSourceWizardStepThree.defaultProps = {
add: false,
error: false,
fulfilled: false,
pending: false,
- name: null
+ name: null,
+ t: translate
};
const makeMapStateToProps = () => {
diff --git a/src/components/addSourceWizard/addSourceWizardStepTwo.js b/src/components/addSourceWizard/addSourceWizardStepTwo.js
index 3d1a45e8..12e141a2 100644
--- a/src/components/addSourceWizard/addSourceWizardStepTwo.js
+++ b/src/components/addSourceWizard/addSourceWizardStepTwo.js
@@ -1,14 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Button, Form, Icon } from 'patternfly-react';
+import { Button, ButtonVariant } from '@patternfly/react-core';
+import { PlusIcon } from '@patternfly/react-icons';
+import { Form } from 'patternfly-react';
import { connect, store, reduxActions, reduxSelectors, reduxTypes } from '../../redux';
import { helpers } from '../../common/helpers';
import apiTypes from '../../constants/apiConstants';
import { dictionary, sslProtocolDictionary } from '../../constants/dictionaryConstants';
import { FormField, fieldValidation } from '../formField/formField';
import { FormState } from '../formState/formState';
-import DropdownSelect from '../dropdownSelect/dropdownSelect';
+import { DropdownSelect, SelectVariant } from '../dropdownSelect/dropdownSelect';
+import { translate } from '../i18n/i18n';
+
+/**
+ * Generate ssl protocol options.
+ *
+ * @type {{title: string|Function, value: *}[]}
+ */
+const sslProtocolOptions = Object.keys(sslProtocolDictionary)
+ .map(type => ({
+ title: () => translate('form-dialog.label', { context: ['option', type] }),
+ value: type
+ }))
+ .concat({
+ title: () => translate('form-dialog.label', { context: ['option', 'disableSsl'] }),
+ value: 'disableSsl'
+ });
+/**
+ * Add a source with a Wizard, step two, apply credentials.
+ */
class AddSourceWizardStepTwo extends React.Component {
static hostValid(value) {
return (
@@ -50,6 +71,7 @@ class AddSourceWizardStepTwo extends React.Component {
getCredentials();
}
+ // ToDo: future, exported hook from addCredentialType can be leveraged here
onAddCredential = () => {
const { type } = this.props;
@@ -275,16 +297,11 @@ class AddSourceWizardStepTwo extends React.Component {
}
renderCredentials({ errors, touched, handleOnEventCustom }) {
- const { availableCredentials, credentials, stepTwoErrorMessages, type } = this.props;
+ const { availableCredentials, credentials, stepTwoErrorMessages, t, type } = this.props;
const multiselectCredentials = type === 'network';
const sourceCredentials = availableCredentials.filter(cred => cred.type === type);
- const titleAddSelect = availableCredentials.length ? 'Select' : 'Add';
- const title = multiselectCredentials
- ? `${titleAddSelect} one or more credentials`
- : `${titleAddSelect} a credential`;
-
const onChangeCredential = event => {
handleOnEventCustom({
name: 'credentials',
@@ -300,20 +317,31 @@ class AddSourceWizardStepTwo extends React.Component {
>
-
- Add
-
-
+ }
+ onClick={this.onAddCredential}
+ title={t('form-dialog.label', { context: 'add-credential' })}
+ />
@@ -364,9 +392,10 @@ class AddSourceWizardStepTwo extends React.Component {
@@ -431,6 +460,14 @@ class AddSourceWizardStepTwo extends React.Component {
}
}
+/**
+ * Prop types
+ *
+ * @type {{add: boolean, optionDisableSsl: boolean, hostsSingle: string, optionParamiko: boolean, credentials: Array,
+ * edit: boolean, stepTwoErrorMessages: object, hosts: Array, optionSslProtocol: string, getCredentials: Function,
+ * type: string, hostsMultiple: string, optionSslCert: boolean, availableCredentials: Array, t: Function,
+ * port: string|number, name: string, id: string|number}}
+ */
AddSourceWizardStepTwo.propTypes = {
add: PropTypes.bool,
availableCredentials: PropTypes.array,
@@ -454,9 +491,17 @@ AddSourceWizardStepTwo.propTypes = {
options: PropTypes.string,
port: PropTypes.string
}),
+ t: PropTypes.func,
type: PropTypes.string
};
+/**
+ * Default props
+ *
+ * @type {{add: boolean, optionDisableSsl: null, hostsSingle: string, optionParamiko: null, credentials: *[], edit: boolean,
+ * stepTwoErrorMessages: {}, hosts: *[], optionSslProtocol: string, getCredentials: Function, type: null,
+ * hostsMultiple: string, optionSslCert: null, availableCredentials: *[], t: translate, port: string, name: string, id: null}}
+ */
AddSourceWizardStepTwo.defaultProps = {
add: true,
availableCredentials: [],
@@ -474,6 +519,7 @@ AddSourceWizardStepTwo.defaultProps = {
optionParamiko: null,
port: '',
stepTwoErrorMessages: {},
+ t: translate,
type: null
};
@@ -496,4 +542,9 @@ const makeMapStateToProps = () => {
const ConnectedAddSourceWizardStepTwo = connect(makeMapStateToProps, mapDispatchToProps)(AddSourceWizardStepTwo);
-export { ConnectedAddSourceWizardStepTwo as default, ConnectedAddSourceWizardStepTwo, AddSourceWizardStepTwo };
+export {
+ ConnectedAddSourceWizardStepTwo as default,
+ ConnectedAddSourceWizardStepTwo,
+ AddSourceWizardStepTwo,
+ sslProtocolOptions
+};
diff --git a/src/components/authentication/__tests__/__snapshots__/authentication.test.js.snap b/src/components/authentication/__tests__/__snapshots__/authentication.test.js.snap
index 50e85f91..8c22e6af 100644
--- a/src/components/authentication/__tests__/__snapshots__/authentication.test.js.snap
+++ b/src/components/authentication/__tests__/__snapshots__/authentication.test.js.snap
@@ -21,6 +21,7 @@ exports[`Authentication Component should render a non-connected component author
"pending": false,
}
}
+ t={[Function]}
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Login error:
- Authentication credentials were not provided
- .
- Please
-
- login
-
- to continue.
-
-
-
-
-
+
+
+
+
+
+
+ Danger alert:
+
+ t(view.error, {"context":"authentication"})
+
+
+ t(view.error-message, {"context":"authentication","message":"Authentication credentials were not provided."}, [object Object])
-
-
+
+
`;
exports[`Authentication Component should render a non-connected component pending: non-connected pending 1`] = `
-
-
-
- Logging in...
-
-
+
+
+ t(view.loading, {"context":"authentication"})
+
`;
diff --git a/src/components/authentication/__tests__/authentication.test.js b/src/components/authentication/__tests__/authentication.test.js
index e70cd67b..d8bcf186 100644
--- a/src/components/authentication/__tests__/authentication.test.js
+++ b/src/components/authentication/__tests__/authentication.test.js
@@ -2,6 +2,7 @@ import React from 'react';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { shallow, mount } from 'enzyme';
+import { Alert } from '@patternfly/react-core';
import { ConnectedAuthentication, Authentication } from '../authentication';
describe('Authentication Component', () => {
@@ -37,7 +38,7 @@ describe('Authentication Component', () => {
);
- expect(component).toMatchSnapshot('non-connected error');
+ expect(component.find(Alert)).toMatchSnapshot('non-connected error');
});
it('should render a non-connected component pending', () => {
diff --git a/src/components/authentication/authentication.js b/src/components/authentication/authentication.js
index e4df0ea5..1bd8dfae 100644
--- a/src/components/authentication/authentication.js
+++ b/src/components/authentication/authentication.js
@@ -1,11 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { Alert, EmptyState, Modal } from 'patternfly-react';
+import { Alert, AlertVariant, Button, Bullseye, EmptyState } from '@patternfly/react-core';
+import { Modal, ModalVariant } from '../modal/modal';
import { reduxActions } from '../../redux';
import helpers from '../../common/helpers';
import { PageLayout } from '../pageLayout/pageLayout';
+import { translate } from '../i18n/i18n';
+/**
+ * Authentication, determine if a user is authorized.
+ */
class Authentication extends React.Component {
componentDidMount() {
const { session, authorizeUser } = this.props;
@@ -16,7 +21,7 @@ class Authentication extends React.Component {
}
render() {
- const { children, session } = this.props;
+ const { children, session, t } = this.props;
if (session.authorized) {
return {children} ;
@@ -24,28 +29,26 @@ class Authentication extends React.Component {
if (session.pending) {
return (
-
-
-
- Logging in...
-
+
+
+ {t('view.loading', { context: 'authentication' })}
);
}
return (
-
-
-
- Login error: {session.errorMessage.replace(/\.$/, '')}
- {session.errorMessage && '.'}
- {!session.authorized && (
-
- Please login to continue.
-
+
+
+ {!session.authorized &&
+ t(
+ 'view.error-message',
+ {
+ context: 'authentication',
+ message: `${session.errorMessage.replace(/\.$/, '')}${session.errorMessage && '.'}` || undefined
+ },
+ [ ]
)}
-
@@ -53,6 +56,11 @@ class Authentication extends React.Component {
}
}
+/**
+ * Prop types
+ *
+ * @type {{authorizeUser: Function, t: Function, children: React.ReactNode, session: object}}
+ */
Authentication.propTypes = {
authorizeUser: PropTypes.func,
children: PropTypes.node.isRequired,
@@ -61,9 +69,16 @@ Authentication.propTypes = {
error: PropTypes.bool,
errorMessage: PropTypes.string,
pending: PropTypes.bool
- })
+ }),
+ t: PropTypes.func
};
+/**
+ * Default props.
+ *
+ * @type {{authorizeUser: Function, t: translate, session: {authorized: boolean, pending: boolean,
+ * errorMessage: string, error: boolean}}}
+ */
Authentication.defaultProps = {
authorizeUser: helpers.noop,
session: {
@@ -71,7 +86,8 @@ Authentication.defaultProps = {
error: false,
errorMessage: '',
pending: false
- }
+ },
+ t: translate
};
const mapDispatchToProps = dispatch => ({
diff --git a/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap b/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap
index a593f4d1..30f5b1ac 100644
--- a/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap
+++ b/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap
@@ -2,84 +2,257 @@
exports[`Confirmation Modal Component should NOT display a confirmation modal: hidden 1`] = `null`;
-exports[`Confirmation Modal Component should display a confirmation modal: show 1`] = `
+exports[`Confirmation Modal Component should allow custom content: custom 1`] = `
+ class="pf-l-bullseye"
+ >
+
+
+
+`;
+
+exports[`Confirmation Modal Component should allow passed children, or specific props: body 1`] = `
+
+
+
+ Warning alert:
+
+
+
+
+
+`;
+
+exports[`Confirmation Modal Component should allow passed children, or specific props: children 1`] = `
+
+`;
+
+exports[`Confirmation Modal Component should allow passed children, or specific props: heading 1`] = `
+
+
+
+
+
+ Warning alert:
+
+ Lorem ipsum
+
+
+
+
+`;
+
+exports[`Confirmation Modal Component should display a confirmation modal: show 1`] = `
+
+
+
+
+
+
+
+
+
- Confirm
+
+ Default alert:
+
+ test
-
-
-
+
diff --git a/src/components/confirmationModal/__tests__/confirmationModal.test.js b/src/components/confirmationModal/__tests__/confirmationModal.test.js
index 68913aad..df6bab04 100644
--- a/src/components/confirmationModal/__tests__/confirmationModal.test.js
+++ b/src/components/confirmationModal/__tests__/confirmationModal.test.js
@@ -1,7 +1,7 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
-import { mount, shallow } from 'enzyme';
+import { shallow } from 'enzyme';
import { ConnectedConfirmationModal, ConfirmationModal } from '../confirmationModal';
describe('Confirmation Modal Component', () => {
@@ -28,7 +28,7 @@ describe('Confirmation Modal Component', () => {
expect(component.find(ConnectedConfirmationModal)).toMatchSnapshot('connected');
});
- it('should display a confirmation modal', () => {
+ it('should display a confirmation modal', async () => {
const onCancel = jest.fn();
const props = {
show: true,
@@ -41,21 +41,55 @@ describe('Confirmation Modal Component', () => {
onCancel
};
- const component = mount( );
-
+ const component = await mountHookComponent( );
expect(component.render()).toMatchSnapshot('show');
- component.find('button[className="btn btn-default"]').simulate('click');
+ component.find('button[className="pf-c-button pf-m-secondary"]').simulate('click');
expect(onCancel).toHaveBeenCalledTimes(1);
});
- it('should NOT display a confirmation modal', () => {
+ it('should NOT display a confirmation modal', async () => {
const props = {
show: false
};
- const component = mount( );
-
+ const component = await mountHookComponent( );
expect(component.render()).toMatchSnapshot('hidden');
});
+
+ it('should allow passed children, or specific props', async () => {
+ const props = {
+ show: true,
+ heading: 'Lorem ipsum',
+ children: 'hello world'
+ };
+
+ const component = await mountHookComponent( );
+ expect(component.find('.pf-c-modal-box__body').render()).toMatchSnapshot('heading');
+
+ component.setProps({
+ heading: null,
+ body: 'Dolor sit'
+ });
+
+ expect(component.find('.pf-c-modal-box__body').render()).toMatchSnapshot('body');
+
+ component.setProps({
+ body: null
+ });
+
+ expect(component.find('.pf-c-modal-box__body').render()).toMatchSnapshot('children');
+ });
+
+ it('should allow custom content', async () => {
+ const props = {
+ show: true,
+ isActions: false,
+ isClose: false,
+ isContentOnly: true
+ };
+
+ const component = await mountHookComponent(lorem ipsum );
+ expect(component.render()).toMatchSnapshot('custom');
+ });
});
diff --git a/src/components/confirmationModal/confirmationModal.js b/src/components/confirmationModal/confirmationModal.js
index f96c996a..d0892dde 100644
--- a/src/components/confirmationModal/confirmationModal.js
+++ b/src/components/confirmationModal/confirmationModal.js
@@ -1,19 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { MessageDialog, Icon } from 'patternfly-react';
+import {
+ Alert,
+ AlertVariant as ConfirmationVariant,
+ Button,
+ ButtonVariant,
+ ModalVariant,
+ Title
+} from '@patternfly/react-core';
+import { Modal } from '../modal/modal';
import { connect, store, reduxTypes } from '../../redux';
-import helpers from '../../common/helpers';
+import { translate } from '../i18n/i18n';
+/**
+ * Display a confirmation modal with actions.
+ *
+ * @param {object} props
+ * @param {React.ReactNode} props.body
+ * @param {React.ReactNode} props.children
+ * @param {string} props.cancelButtonText
+ * @param {string} props.confirmButtonText
+ * @param {React.ReactNode} props.heading
+ * @param {string} props.icon
+ * @param {boolean} props.isClose
+ * @param {boolean} props.isContentOnly
+ * @param {boolean} props.isActions
+ * @param {Function} props.onCancel
+ * @param {Function} props.onConfirm
+ * @param {boolean} props.show
+ * @param {Function} props.t
+ * @param {string} props.title
+ * @param {string} props.variant
+ * @returns {React.ReactNode}
+ */
const ConfirmationModal = ({
- show,
- title,
- heading,
body,
- icon,
- confirmButtonText,
+ children,
cancelButtonText,
+ confirmButtonText,
+ heading,
+ icon,
+ isClose,
+ isContentOnly,
+ isActions,
+ onCancel,
onConfirm,
- onCancel
+ show,
+ t,
+ title,
+ variant
}) => {
const cancel = () => {
if (onCancel) {
@@ -25,47 +60,112 @@ const ConfirmationModal = ({
}
};
+ const setActions = () => {
+ const actions = [];
+
+ if (!isActions) {
+ return actions;
+ }
+
+ if (onConfirm) {
+ actions.push(
+
+ {confirmButtonText || t('form-dialog.label', { context: ['submit', 'confirmation'] })}
+
+ );
+ }
+
+ actions.push(
+
+ {cancelButtonText || t('form-dialog.label', { context: 'cancel' })}
+
+ );
+
+ return actions;
+ };
+
+ const updatedChildren =
+ body || heading ? (
+
+ {body}
+
+ ) : (
+ children
+ );
+
return (
- {heading}}
- secondaryContent={{body}
}
- />
+ {title || t('form-dialog.label', { context: ['submit', 'confirmation'] })}
+ )
+ }
+ isContentOnly={isContentOnly}
+ isOpen={show}
+ onClose={cancel}
+ showClose={isClose}
+ variant={variant}
+ >
+ {updatedChildren || ''}
+
);
};
+/**
+ * Prop types
+ *
+ * @type {{isClose: boolean, isActions: boolean, heading: React.ReactNode, icon: string, body: React.ReactNode, title: string,
+ * show: boolean, t: Function, children: React.ReactNode, onCancel: Function, onConfirm: Function, variant: string,
+ * isContentOnly: boolean, cancelButtonText: string, confirmButtonText: string}}
+ */
ConfirmationModal.propTypes = {
- show: PropTypes.bool.isRequired,
- title: PropTypes.string,
- heading: PropTypes.node,
- icon: PropTypes.node,
body: PropTypes.node,
- confirmButtonText: PropTypes.string,
cancelButtonText: PropTypes.string,
+ children: PropTypes.node,
+ confirmButtonText: PropTypes.string,
+ heading: PropTypes.node,
+ icon: PropTypes.oneOf([...Object.values(ConfirmationVariant)]),
+ isActions: PropTypes.bool,
+ isClose: PropTypes.bool,
+ isContentOnly: PropTypes.bool,
+ show: PropTypes.bool.isRequired,
+ onCancel: PropTypes.func,
onConfirm: PropTypes.func,
- onCancel: PropTypes.func
+ t: PropTypes.func,
+ title: PropTypes.string,
+ variant: PropTypes.oneOf([...Object.values(ModalVariant)])
};
+/**
+ * Default props.
+ *
+ * @type {{isClose: boolean, isActions: boolean, heading: null, icon: ConfirmationVariant.warning, title: null, body: null,
+ * t: translate, children: null, onCancel: null, onConfirm: null, variant: null, isContentOnly: boolean,
+ * confirmButtonText: null, cancelButtonText: null}}
+ */
ConfirmationModal.defaultProps = {
- title: 'Confirm',
+ children: null,
+ title: null,
heading: null,
body: null,
- icon: ,
- confirmButtonText: 'Confirm',
- cancelButtonText: '',
- onConfirm: helpers.noop,
- onCancel: null
+ icon: ConfirmationVariant.warning,
+ isActions: true,
+ isClose: true,
+ isContentOnly: false,
+ confirmButtonText: null,
+ cancelButtonText: null,
+ onConfirm: null,
+ onCancel: null,
+ t: translate,
+ variant: null
};
const mapStateToProps = state => ({ ...state.confirmationModal });
const ConnectedConfirmationModal = connect(mapStateToProps)(ConfirmationModal);
-export { ConnectedConfirmationModal as default, ConnectedConfirmationModal, ConfirmationModal };
+export { ConnectedConfirmationModal as default, ConnectedConfirmationModal, ConfirmationModal, ConfirmationVariant };
diff --git a/src/components/contextIcon/__tests__/__snapshots__/contextIcon.test.js.snap b/src/components/contextIcon/__tests__/__snapshots__/contextIcon.test.js.snap
new file mode 100644
index 00000000..a1ad1def
--- /dev/null
+++ b/src/components/contextIcon/__tests__/__snapshots__/contextIcon.test.js.snap
@@ -0,0 +1,209 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, canceled 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, cancelled 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, completed 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, created 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, download 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, failed 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, idCard 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, network 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, paused 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, pencil 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, pending 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, running 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, satellite 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, scans 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, sources 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, success 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, trash 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, unknown 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, unreachable 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: icon variant, vcenter 1`] = `
+
+`;
+
+exports[`ContextIcon Component should export, and handle icon variants: variants 1`] = `
+Object {
+ "canceled": "failed",
+ "cancelled": "failed",
+ "completed": "success",
+ "created": "pending",
+ "download": "download",
+ "failed": "failed",
+ "idCard": "idCard",
+ "network": "network",
+ "paused": "paused",
+ "pencil": "pencil",
+ "pending": "pending",
+ "running": "pending",
+ "satellite": "satellite",
+ "scans": "scans",
+ "sources": "sources",
+ "success": "success",
+ "trash": "trash",
+ "unknown": "unknown",
+ "unreachable": "unreachable",
+ "vcenter": "vcenter",
+}
+`;
+
+exports[`ContextIcon Component should render a basic component with default props: basic 1`] = `
+
+`;
diff --git a/src/components/contextIcon/__tests__/__snapshots__/contextIconAction.test.js.snap b/src/components/contextIcon/__tests__/__snapshots__/contextIconAction.test.js.snap
new file mode 100644
index 00000000..1e60c279
--- /dev/null
+++ b/src/components/contextIcon/__tests__/__snapshots__/contextIconAction.test.js.snap
@@ -0,0 +1,95 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ContextIconAction Component should export, and handle icon variants: icon variant, canceled 1`] = `
+
+`;
+
+exports[`ContextIconAction Component should export, and handle icon variants: icon variant, cancelled 1`] = `
+
+`;
+
+exports[`ContextIconAction Component should export, and handle icon variants: icon variant, completed 1`] = `
+
+`;
+
+exports[`ContextIconAction Component should export, and handle icon variants: icon variant, created 1`] = `
+
+`;
+
+exports[`ContextIconAction Component should export, and handle icon variants: icon variant, failed 1`] = `
+
+`;
+
+exports[`ContextIconAction Component should export, and handle icon variants: icon variant, paused 1`] = `
+
+`;
+
+exports[`ContextIconAction Component should export, and handle icon variants: icon variant, pending 1`] = `
+
+`;
+
+exports[`ContextIconAction Component should export, and handle icon variants: icon variant, play 1`] = `
+
+`;
+
+exports[`ContextIconAction Component should export, and handle icon variants: icon variant, running 1`] = `
+
+`;
+
+exports[`ContextIconAction Component should export, and handle icon variants: variants 1`] = `
+Object {
+ "canceled": "failed",
+ "cancelled": "failed",
+ "completed": "completed",
+ "created": "running",
+ "failed": "failed",
+ "paused": "paused",
+ "pending": "pending",
+ "play": "play",
+ "running": "running",
+}
+`;
+
+exports[`ContextIconAction Component should render a basic component with default props: basic 1`] = `
+
+`;
diff --git a/src/components/contextIcon/__tests__/contextIcon.test.js b/src/components/contextIcon/__tests__/contextIcon.test.js
new file mode 100644
index 00000000..e998def5
--- /dev/null
+++ b/src/components/contextIcon/__tests__/contextIcon.test.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { ContextIcon, ContextIconVariant } from '../contextIcon';
+
+describe('ContextIcon Component', () => {
+ it('should render a basic component with default props', () => {
+ const component = shallow( );
+ expect(component).toMatchSnapshot('basic');
+ });
+
+ it('should export, and handle icon variants', () => {
+ expect(ContextIconVariant).toMatchSnapshot('variants');
+
+ Object.entries(ContextIconVariant).forEach(([key, value]) => {
+ const component = shallow( );
+ expect(component).toMatchSnapshot(`icon variant, ${key}`);
+ });
+ });
+});
diff --git a/src/components/contextIcon/__tests__/contextIconAction.test.js b/src/components/contextIcon/__tests__/contextIconAction.test.js
new file mode 100644
index 00000000..55e5a668
--- /dev/null
+++ b/src/components/contextIcon/__tests__/contextIconAction.test.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { ContextIconAction, ContextIconActionVariant } from '../contextIconAction';
+
+describe('ContextIconAction Component', () => {
+ it('should render a basic component with default props', () => {
+ const component = shallow( );
+ expect(component).toMatchSnapshot('basic');
+ });
+
+ it('should export, and handle icon variants', () => {
+ expect(ContextIconActionVariant).toMatchSnapshot('variants');
+
+ Object.entries(ContextIconActionVariant).forEach(([key, value]) => {
+ const component = shallow( );
+ expect(component).toMatchSnapshot(`icon variant, ${key}`);
+ });
+ });
+});
diff --git a/src/components/contextIcon/contextIcon.js b/src/components/contextIcon/contextIcon.js
new file mode 100644
index 00000000..5071ff4a
--- /dev/null
+++ b/src/components/contextIcon/contextIcon.js
@@ -0,0 +1,165 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Spinner } from '@patternfly/react-core';
+import {
+ CheckCircleIcon,
+ DisconnectedIcon,
+ DownloadIcon,
+ ErrorCircleOIcon,
+ ExclamationTriangleIcon,
+ IdCardIcon,
+ PencilAltIcon,
+ PficonNetworkRangeIcon,
+ PficonSatelliteIcon,
+ PficonVcenterIcon,
+ TrashIcon,
+ UnknownIcon,
+ IconSize,
+ ClipboardCheckIcon,
+ CrosshairsIcon
+} from '@patternfly/react-icons';
+import {
+ global_Color_dark_100 as gray,
+ global_success_color_100 as green,
+ global_warning_color_100 as yellow,
+ global_danger_color_100 as red
+} from '@patternfly/react-tokens';
+
+/**
+ * Context icon colors, for consistency
+ *
+ * @type {{ red: object, gray: object, green: object, yellow: object }}
+ */
+const ContextIconColors = {
+ gray,
+ green,
+ yellow,
+ red
+};
+
+/**
+ * Context icon variants
+ *
+ * @type {{running: string, canceled: string, paused: string, unreachable: string, success: string, created: string,
+ * pending: string, cancelled: string, completed: string, failed: string}}
+ */
+const ContextIconVariant = {
+ completed: 'success',
+ success: 'success',
+ failed: 'failed',
+ canceled: 'failed',
+ cancelled: 'failed',
+ created: 'pending',
+ pending: 'pending',
+ running: 'pending',
+ paused: 'paused',
+ download: 'download',
+ idCard: 'idCard',
+ network: 'network',
+ pencil: 'pencil',
+ satellite: 'satellite',
+ scans: 'scans',
+ sources: 'sources',
+ trash: 'trash',
+ unknown: 'unknown',
+ unreachable: 'unreachable',
+ vcenter: 'vcenter'
+};
+
+/**
+ * Emulate pf icon sizing for custom SVGs
+ *
+ * @param {string} size
+ * @returns {string} em measurement
+ */
+const svgSize = size => {
+ if (!Number.isNaN(Number.parseFloat(size))) {
+ return size;
+ }
+
+ switch (size) {
+ case 'md':
+ return '1.5em';
+ case 'lg':
+ return '2em';
+ case 'xl':
+ return '3em';
+ case 'sm':
+ default:
+ return '1em';
+ }
+};
+
+/**
+ * Return an icon from context/symbol
+ *
+ * @param {object} props
+ * @param {string} props.symbol
+ * @param {object} props.props
+ * @returns {React.ReactNode}
+ */
+const ContextIcon = ({ symbol, ...props }) => {
+ switch (symbol) {
+ case ContextIconVariant.download:
+ return ;
+ case ContextIconVariant.failed:
+ return ;
+ case ContextIconVariant.idCard:
+ return ;
+ case ContextIconVariant.network:
+ return ;
+ case ContextIconVariant.paused:
+ return ;
+ case ContextIconVariant.pencil:
+ return ;
+ case ContextIconVariant.pending:
+ const updatedSize = { style: { display: 'inline-block' } };
+
+ if (props.size) {
+ updatedSize.size = undefined;
+ updatedSize.style.height = svgSize(props.size);
+ updatedSize.style.width = svgSize(props.size);
+ }
+
+ return ;
+ case ContextIconVariant.satellite:
+ return ;
+ case ContextIconVariant.scans:
+ return ;
+ case ContextIconVariant.sources:
+ return ;
+ case ContextIconVariant.success:
+ return ;
+ case ContextIconVariant.trash:
+ return ;
+ case ContextIconVariant.unreachable:
+ return ;
+ case ContextIconVariant.vcenter:
+ return ;
+ case ContextIconVariant.unknown:
+ default:
+ return ;
+ }
+};
+
+/**
+ * Prop types
+ *
+ * @type {{symbol: string}}
+ */
+ContextIcon.propTypes = {
+ symbol: PropTypes.oneOf([...Object.values(ContextIconVariant)]),
+ size: PropTypes.oneOf([...Object.values(IconSize)])
+};
+
+/**
+ * Default props
+ *
+ * @type {{symbol: null}}
+ */
+ContextIcon.defaultProps = {
+ symbol: null,
+ size: IconSize.sm
+};
+
+export { ContextIcon as default, ContextIcon, ContextIconColors, ContextIconVariant };
diff --git a/src/components/contextIcon/contextIconAction.js b/src/components/contextIcon/contextIconAction.js
new file mode 100644
index 00000000..ed9eea41
--- /dev/null
+++ b/src/components/contextIcon/contextIconAction.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { PauseIcon, PlayIcon, RedoIcon, StopIcon } from '@patternfly/react-icons';
+
+/**
+ * Context icon variants
+ *
+ * @type {{running: string, play: string, canceled: string, paused: string, created: string, pending: string,
+ * cancelled: string, completed: string, failed: string}}
+ */
+const ContextIconActionVariant = {
+ completed: 'completed',
+ failed: 'failed',
+ canceled: 'failed',
+ cancelled: 'failed',
+ pending: 'pending',
+ created: 'running',
+ running: 'running',
+ paused: 'paused',
+ play: 'play'
+};
+
+/**
+ * Return an action icon from context/symbol
+ *
+ * @param {object} props
+ * @param {string} props.symbol
+ * @param {object} props.props
+ * @returns {React.ReactNode}
+ */
+const ContextIconAction = ({ symbol, ...props }) => {
+ switch (symbol) {
+ case ContextIconActionVariant.completed:
+ case ContextIconActionVariant.failed:
+ return ;
+ case ContextIconActionVariant.running:
+ return ;
+ case ContextIconActionVariant.paused:
+ return ;
+ case ContextIconActionVariant.pending:
+ return ;
+ default:
+ return ;
+ }
+};
+
+/**
+ * Prop types
+ *
+ * @type {{symbol: string}}
+ */
+ContextIconAction.propTypes = {
+ symbol: PropTypes.oneOf([...Object.values(ContextIconActionVariant)])
+};
+
+/**
+ * Default props
+ *
+ * @type {{symbol: null}}
+ */
+ContextIconAction.defaultProps = {
+ symbol: null
+};
+
+export { ContextIconAction as default, ContextIconAction, ContextIconActionVariant };
diff --git a/src/components/createCredentialDialog/__tests__/__snapshots__/createCredentialDialog.test.js.snap b/src/components/createCredentialDialog/__tests__/__snapshots__/createCredentialDialog.test.js.snap
index 7cc8ee11..812eed9d 100644
--- a/src/components/createCredentialDialog/__tests__/__snapshots__/createCredentialDialog.test.js.snap
+++ b/src/components/createCredentialDialog/__tests__/__snapshots__/createCredentialDialog.test.js.snap
@@ -1,48 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`CreateCredentialDialog Component should export select options: options 1`] = `
+Array [
+ Object {
+ "title": "t(form-dialog.label_option, {\\"context\\":\\"sshKey\\"})",
+ "value": "sshKey",
+ },
+ Object {
+ "title": "t(form-dialog.label_option, {\\"context\\":\\"usernamePassword\\"})",
+ "value": "usernamePassword",
+ },
+]
+`;
+
exports[`CreateCredentialDialog Component should render a connected component: connected 1`] = `
-
-
-
-
+
+
+
+
+
@@ -99,55 +128,51 @@ exports[`CreateCredentialDialog Component should render a connected component: c
class="col-sm-7"
>
-
-
- Username and Password
-
-
-
-
-
+
- Username and Password
-
-
-
+
+
+
+
+
+
@@ -191,24 +216,32 @@ exports[`CreateCredentialDialog Component should render a connected component: c
-
+
diff --git a/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js b/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js
index 510666e1..ad9dda76 100644
--- a/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js
+++ b/src/components/createCredentialDialog/__tests__/createCredentialDialog.test.js
@@ -2,7 +2,11 @@ import React from 'react';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { mount, shallow } from 'enzyme';
-import { ConnectedCreateCredentialDialog, CreateCredentialDialog } from '../createCredentialDialog';
+import {
+ ConnectedCreateCredentialDialog,
+ CreateCredentialDialog,
+ authenticationTypeOptions
+} from '../createCredentialDialog';
describe('CreateCredentialDialog Component', () => {
const generateEmptyStore = (obj = {}) => configureMockStore()(obj);
@@ -27,4 +31,13 @@ describe('CreateCredentialDialog Component', () => {
const component = shallow( );
expect(component.render()).toMatchSnapshot('non-connected');
});
+
+ it('should export select options', () => {
+ expect(
+ authenticationTypeOptions.map(({ title, ...option }) => ({
+ ...option,
+ title: (typeof title === 'function' && title()) || title
+ }))
+ ).toMatchSnapshot('options');
+ });
});
diff --git a/src/components/createCredentialDialog/createCredentialDialog.js b/src/components/createCredentialDialog/createCredentialDialog.js
index 76ae544f..468df51f 100644
--- a/src/components/createCredentialDialog/createCredentialDialog.js
+++ b/src/components/createCredentialDialog/createCredentialDialog.js
@@ -1,11 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Modal, Alert, Button, Icon, Form, Grid } from 'patternfly-react';
+import { Alert, Button, ButtonVariant, Title } from '@patternfly/react-core';
+import { Form, Grid } from 'patternfly-react';
+import { Modal } from '../modal/modal';
import { connect, reduxActions, reduxTypes, store } from '../../redux';
import { helpers } from '../../common/helpers';
import { authDictionary, dictionary } from '../../constants/dictionaryConstants';
-import DropdownSelect from '../dropdownSelect/dropdownSelect';
-
+import { DropdownSelect } from '../dropdownSelect/dropdownSelect';
+import { translate } from '../i18n/i18n';
+
+/**
+ * Generate authentication type options.
+ *
+ * @type {{title: string|Function, value: *}[]}
+ */
+const authenticationTypeOptions = Object.keys(authDictionary).map(type => ({
+ title: () => translate('form-dialog.label', { context: ['option', type] }),
+ value: type
+}));
+
+/**
+ * Create or edit a credential.
+ */
class CreateCredentialDialog extends React.Component {
static renderFormLabel(label) {
return (
@@ -74,7 +90,8 @@ class CreateCredentialDialog extends React.Component {
state = { ...this.initialState };
- UNSAFE_componentWillReceiveProps(nextProps) { //eslint-disable-line
+ // eslint-disable-next-line camelcase
+ UNSAFE_componentWillReceiveProps(nextProps) {
const { edit, fulfilled, getCredentials, show, viewOptions } = this.props;
if (!show && nextProps.show) {
@@ -348,10 +365,10 @@ class CreateCredentialDialog extends React.Component {
@@ -386,8 +403,8 @@ class CreateCredentialDialog extends React.Component {
if (error) {
return (
-
- Error {errorMessage}
+
+ {errorMessage}
);
}
@@ -396,7 +413,7 @@ class CreateCredentialDialog extends React.Component {
}
render() {
- const { show, edit } = this.props;
+ const { show, edit, t } = this.props;
const {
credentialType,
credentialName,
@@ -408,14 +425,20 @@ class CreateCredentialDialog extends React.Component {
} = this.state;
return (
-
-
-
-
+ {edit ? `View Credential - ${credentialName}` : 'Add Credential'}}
+ actions={[
+
+ {t('form-dialog.label', { context: ['submit', 'create-credential'] })}
+ ,
+
+ {t('form-dialog.label', { context: 'cancel' })}
- {edit ? `View Credential - ${credentialName}` : 'Add Credential'}
-
-
+ ]}
+ >
{this.renderErrorMessage()}
@@ -450,10 +473,10 @@ class CreateCredentialDialog extends React.Component {
@@ -474,14 +497,6 @@ class CreateCredentialDialog extends React.Component {
{this.renderNetworkForm()}
-
-
- Cancel
-
-
- Save
-
-
);
}
@@ -499,6 +514,7 @@ CreateCredentialDialog.propTypes = {
fulfilled: PropTypes.bool,
error: PropTypes.bool,
errorMessage: PropTypes.string,
+ t: PropTypes.func,
viewOptions: PropTypes.object
};
@@ -514,6 +530,7 @@ CreateCredentialDialog.defaultProps = {
fulfilled: false,
error: false,
errorMessage: null,
+ t: translate,
viewOptions: {}
};
@@ -530,4 +547,9 @@ const mapStateToProps = state => ({
const ConnectedCreateCredentialDialog = connect(mapStateToProps, mapDispatchToProps)(CreateCredentialDialog);
-export { ConnectedCreateCredentialDialog as default, ConnectedCreateCredentialDialog, CreateCredentialDialog };
+export {
+ ConnectedCreateCredentialDialog as default,
+ ConnectedCreateCredentialDialog,
+ CreateCredentialDialog,
+ authenticationTypeOptions
+};
diff --git a/src/components/createScanDialog/__tests__/__snapshots__/createScanDialog.test.js.snap b/src/components/createScanDialog/__tests__/__snapshots__/createScanDialog.test.js.snap
index 20285467..1f2c635b 100644
--- a/src/components/createScanDialog/__tests__/__snapshots__/createScanDialog.test.js.snap
+++ b/src/components/createScanDialog/__tests__/__snapshots__/createScanDialog.test.js.snap
@@ -16,16 +16,44 @@ Object {
exports[`CreateScanDialog Component should handle multiple error responses: basic error 1`] = `
-
-
+
+
+
+ Danger alert:
+
Error
-
- lorem ipsum
+
+
+ lorem ipsum
+
`;
@@ -64,307 +92,329 @@ exports[`CreateScanDialog Component should render a connected component with def
exports[`CreateScanDialog Component should render a non-connected component: non-connected 1`] = `
diff --git a/src/components/createScanDialog/__tests__/createScanDialog.test.js b/src/components/createScanDialog/__tests__/createScanDialog.test.js
index 20c2a98e..3f3644e9 100644
--- a/src/components/createScanDialog/__tests__/createScanDialog.test.js
+++ b/src/components/createScanDialog/__tests__/createScanDialog.test.js
@@ -52,7 +52,7 @@ describe('CreateScanDialog Component', () => {
};
const component = mount( );
- expect(component.find('div[className~="alert-danger"]').render()).toMatchSnapshot('basic error');
+ expect(component.find('div[className*="danger"]').render()).toMatchSnapshot('basic error');
component.setProps({ submitErrorMessages: { scanName: 'lorem ipsum' } });
expect(component.find('div[className~="has-error"]').render()).toMatchSnapshot('named error');
diff --git a/src/components/createScanDialog/createScanDialog.js b/src/components/createScanDialog/createScanDialog.js
index 4ae395d9..6a69bc27 100644
--- a/src/components/createScanDialog/createScanDialog.js
+++ b/src/components/createScanDialog/createScanDialog.js
@@ -1,12 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Alert, Button, FieldLevelHelp, Form, Icon, Modal, Spinner } from 'patternfly-react';
+import { Alert, AlertVariant, Button, ButtonVariant, Title } from '@patternfly/react-core';
+import { FieldLevelHelp, Form, Spinner } from 'patternfly-react';
+import { Modal } from '../modal/modal';
import { connect, reduxActions, reduxTypes, store } from '../../redux';
import { FormState } from '../formState/formState';
import { FormField, fieldValidation } from '../formField/formField';
import { TouchSpin } from '../touchspin/touchspin';
import helpers from '../../common/helpers';
import apiTypes from '../../constants/apiConstants';
+import { translate } from '../i18n/i18n';
class CreateScanDialog extends React.Component {
onClose = () => {
@@ -139,7 +142,7 @@ class CreateScanDialog extends React.Component {
if (!props.show) {
store.dispatch({
type: reduxTypes.toastNotifications.TOAST_ADD,
- alertType: 'error',
+ alertType: 'danger',
header: `Error creating scan ${values.scanName}`,
message: props.errorMessage
});
@@ -309,8 +312,8 @@ class CreateScanDialog extends React.Component {
if (error && !Object.keys(submitErrorMessages).length) {
return (
-
- Error {errorMessage}
+
+ {errorMessage}
);
}
@@ -319,63 +322,70 @@ class CreateScanDialog extends React.Component {
}
render() {
- const { pending, show, sources } = this.props;
+ const { pending, show, sources, t } = this.props;
if (!sources || sources.length === 0 || !sources[0]) {
return null;
}
+ const formActions = (onSubmit, isValid) => {
+ const updatedActions = [];
+ if (!pending) {
+ updatedActions.push(
+
+ {t('form-dialog.label', { context: ['submit', 'create-scan'] })}
+
+ );
+ updatedActions.push(
+
+ {t('form-dialog.label', { context: 'cancel' })}
+
+ );
+ }
+ return updatedActions;
+ };
+
return (
-
- item.name).join(', '),
- scanConcurrency: 25,
- scanDirectories: [],
- jbossEap: false,
- jbossFuse: false,
- jbossWs: false,
- jbossBrms: false,
- scanName: '',
- scanSources: sources.map(item => item.id)
- }}
- validate={this.onValidateForm}
- onSubmit={this.onSubmit}
- >
- {({ handleOnSubmit, isValid, ...options }) => (
+ item.name).join(', '),
+ scanConcurrency: 25,
+ scanDirectories: [],
+ jbossEap: false,
+ jbossFuse: false,
+ jbossWs: false,
+ jbossBrms: false,
+ scanName: '',
+ scanSources: sources.map(item => item.id)
+ }}
+ validate={this.onValidateForm}
+ onSubmit={this.onSubmit}
+ >
+ {({ handleOnSubmit, isValid, ...options }) => (
+ Scan}
+ actions={formActions(handleOnSubmit, isValid)}
+ >
-
-
-
-
- Scan
-
-
- {pending && (
-
-
- Scan updating...
-
- )}
- {!pending && this.renderErrorMessage(options)}
- {!pending && this.renderNameSources(options)}
- {!pending && this.renderConcurrentScans(options)}
- {!pending && this.renderAdditionalProducts(options)}
-
-
-
- Cancel
-
-
- Scan
-
-
+ {pending && (
+
+
+ Scan updating...
+
+ )}
+ {!pending && this.renderErrorMessage(options)}
+ {!pending && this.renderNameSources(options)}
+ {!pending && this.renderConcurrentScans(options)}
+ {!pending && this.renderAdditionalProducts(options)}
- )}
-
-
+
+ )}
+
);
}
}
@@ -394,7 +404,8 @@ CreateScanDialog.propTypes = {
scanDirectories: PropTypes.string,
scanName: PropTypes.string,
scanSources: PropTypes.string
- })
+ }),
+ t: PropTypes.func
};
CreateScanDialog.defaultProps = {
@@ -404,7 +415,8 @@ CreateScanDialog.defaultProps = {
fulfilled: false,
pending: false,
startScan: helpers.noop,
- submitErrorMessages: {}
+ submitErrorMessages: {},
+ t: translate
};
const mapDispatchToProps = dispatch => ({
diff --git a/src/components/credentials/__tests__/__snapshots__/credentialsEmptyState.test.js.snap b/src/components/credentials/__tests__/__snapshots__/credentialsEmptyState.test.js.snap
index 64361178..fafa5379 100644
--- a/src/components/credentials/__tests__/__snapshots__/credentialsEmptyState.test.js.snap
+++ b/src/components/credentials/__tests__/__snapshots__/credentialsEmptyState.test.js.snap
@@ -2,123 +2,118 @@
exports[`CredentialsEmptyState Component should render a basic component 1`] = `
+
+
+
+
+ t(view.empty-state, {"context":"title","name":"Quipucords"})
+
+ t(view.empty-state_description, {"context":"credentials","name":"The Quipucords tool"})
+
+
-
-
-
- Welcome to Quipucords
-
-
- Credentials contain authentication information needed to scan a source. A credential includes
-
- a username and a password or SSH key. The Quipucords tool uses SSH to connect to servers
-
- on the network and uses credentials to access those servers.
-
-
-
-
+
- Add Credential
-
-
-
+
+
+
+
+
+
-
+
+
-
- Add Source
-
-
+ t(view.empty-state_label, {"context":"source"})
+
`;
exports[`CredentialsEmptyState Component should render the application name: application name 1`] = `
-
-
- Welcome to
- Ipsum
-
-
+ t(view.empty-state, {"context":"title","name":"Ipsum"})
+
+
`;
diff --git a/src/components/credentials/__tests__/__snapshots__/credentialsListItem.test.js.snap b/src/components/credentials/__tests__/__snapshots__/credentialsListItem.test.js.snap
index 2cfcfbb1..23e4eb97 100644
--- a/src/components/credentials/__tests__/__snapshots__/credentialsListItem.test.js.snap
+++ b/src/components/credentials/__tests__/__snapshots__/credentialsListItem.test.js.snap
@@ -28,56 +28,40 @@ exports[`CredentialListItem Component should render a non-connected component: n
actions={
Array [
-
,
-
,
@@ -127,17 +111,11 @@ exports[`CredentialListItem Component should render a non-connected component: n
className="quipucords-description-right"
>
Username and Password
@@ -150,17 +128,11 @@ exports[`CredentialListItem Component should render a non-connected component: n
key="1"
leftContent={
-
,
-
,
@@ -283,17 +239,11 @@ exports[`CredentialListItem Component should render a non-connected component: n
className="quipucords-description-right"
>
Username and Password
@@ -303,17 +253,11 @@ exports[`CredentialListItem Component should render a non-connected component: n
heading={null}
leftContent={
-
- View Credential
-
- }
- placement="top"
- rootClose={true}
- trigger={
- Array [
- "hover",
- ]
- }
+
-
+
+
+ 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
-
- }
- placement="top"
- rootClose={true}
- trigger={
- Array [
- "hover",
- ]
- }
+
-
+
+
+ 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}
>
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -520,67 +639,130 @@ exports[`CredentialListItem Component should render a non-connected component: n
className="list-view-pf-left"
>
-
- Network
-
- }
- placement="top"
- rootClose={true}
- trigger={
- Array [
- "hover",
- ]
- }
+
-
+
+
+ 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}
>
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -612,46 +794,104 @@ exports[`CredentialListItem Component should render a non-connected component: n
className="quipucords-description-right"
>
-
- Authorization Type
-
- }
- placement="top"
- rootClose={true}
- trigger={
- Array [
- "hover",
- ]
- }
+
-
+
+
+ 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
-
-
+
+
+ Username and Password
+
+
+
+
diff --git a/src/components/credentials/__tests__/credentialsEmptyState.test.js b/src/components/credentials/__tests__/credentialsEmptyState.test.js
index 349c798c..777ae38d 100644
--- a/src/components/credentials/__tests__/credentialsEmptyState.test.js
+++ b/src/components/credentials/__tests__/credentialsEmptyState.test.js
@@ -1,6 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
-import { EmptyState } from 'patternfly-react';
+import { Title } from '@patternfly/react-core';
import CredentialsEmptyState from '../credentialsEmptyState';
describe('CredentialsEmptyState Component', () => {
@@ -18,6 +18,6 @@ describe('CredentialsEmptyState Component', () => {
};
const component = mount( );
- expect(component.find(EmptyState.Title)).toMatchSnapshot('application name');
+ expect(component.find(Title)).toMatchSnapshot('application name');
});
});
diff --git a/src/components/credentials/credentialListItem.js b/src/components/credentials/credentialListItem.js
index ba2cb9e9..1998aef8 100644
--- a/src/components/credentials/credentialListItem.js
+++ b/src/components/credentials/credentialListItem.js
@@ -1,7 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
-import { ListView, Button, Grid, Icon, Checkbox } from 'patternfly-react';
+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';
@@ -71,26 +73,26 @@ class CredentialListItem extends React.Component {
const { item, onEdit, onDelete } = this.props;
return [
-
+
{
onEdit(item);
}}
- bsStyle="link"
- key="editButton"
+ variant={ButtonVariant.plain}
>
-
+
,
-
+
{
onDelete(item);
}}
- bsStyle="link"
- key="removeButton"
+ variant={ButtonVariant.plain}
>
-
+
];
@@ -127,21 +129,13 @@ class CredentialListItem extends React.Component {
case 'sources':
(item.sources || []).sort((item1, item2) => item1.name.localeCompare(item2.name));
return (
-
- {item.sources &&
- item.sources.map(source => (
-
-
-
-
-
-
- {source.name}
-
-
-
- ))}
-
+
+ {item?.sources?.map(source => (
+ }>
+ {source.name}
+
+ ))}
+
);
default:
return null;
@@ -154,7 +148,7 @@ class CredentialListItem extends React.Component {
const sourceTypeIcon = helpers.sourceTypeIcon(item.cred_type);
const leftContent = (
-
+
);
@@ -165,7 +159,7 @@ class CredentialListItem extends React.Component {
{item.name}
- {dictionary[CredentialListItem.authType(item)]}
+ {dictionary[CredentialListItem.authType(item)]}
);
diff --git a/src/components/credentials/credentials.js b/src/components/credentials/credentials.js
index 36ca4fa3..77c7b006 100644
--- a/src/components/credentials/credentials.js
+++ b/src/components/credentials/credentials.js
@@ -1,9 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Alert, Button, DropdownButton, EmptyState, Form, Grid, ListView, MenuItem, Modal } from 'patternfly-react';
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 { 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 ViewToolbar from '../viewToolbar/viewToolbar';
@@ -11,6 +31,7 @@ import ViewPaginationRow from '../viewPaginationRow/viewPaginationRow';
import CredentialsEmptyState from './credentialsEmptyState';
import CredentialListItem from './credentialListItem';
import { CredentialFilterFields, CredentialSortFields } from './credentialConstants';
+import { translate } from '../i18n/i18n';
class Credentials extends React.Component {
credentialsToDelete = [];
@@ -62,7 +83,7 @@ class Credentials extends React.Component {
if (nextProps.update.error && !update.error) {
store.dispatch({
type: reduxTypes.toastNotifications.TOAST_ADD,
- alertType: 'error',
+ alertType: 'danger',
header: 'Error',
message: (
@@ -77,13 +98,6 @@ class Credentials extends React.Component {
}
}
- onAddCredential = credentialType => {
- store.dispatch({
- type: reduxTypes.credentials.CREATE_CREDENTIAL_SHOW,
- credentialType
- });
- };
-
onDeleteCredentials = () => {
const { viewOptions } = this.props;
@@ -100,16 +114,14 @@ class Credentials extends React.Component {
});
const body = (
-
-
-
+
);
const onConfirm = () => this.doDeleteCredentials(viewOptions.selectedItems);
@@ -191,41 +203,34 @@ class Credentials extends React.Component {
}
renderCredentialActions() {
- const { viewOptions } = this.props;
+ const { t, viewOptions } = this.props;
return (
-
- this.onAddCredential('network')}>
- Network Credential
-
- this.onAddCredential('satellite')}>
- Satellite Credential
-
- this.onAddCredential('vcenter')}>
- VCenter Credential
-
-
+
{' '}
- Delete
+ {t('form-dialog.label', { context: 'delete' })}
);
}
renderPendingMessage() {
- const { pending } = this.props;
+ const { pending, t } = this.props;
if (pending) {
return (
-
-
-
- Loading credentials...
-
+
+
+ {t('view.loading', { context: 'credentials' })}
);
}
@@ -234,6 +239,8 @@ class Credentials extends React.Component {
}
renderCredentialsList(items) {
+ const { t } = this.props;
+
if (_size(items)) {
return (
@@ -250,29 +257,35 @@ class Credentials extends React.Component {
}
return (
-
- No Results Match the Filter Criteria
- The active filters are hiding all items.
-
-
- Clear Filters
+
+
+
+ {t('view.empty-state', { context: ['filter', 'title'] })}
+
+ {t('view.empty-state', { context: ['filter', 'description'] })}
+
+
+ {t('view.empty-state', { context: ['label', 'clear'] })}
-
+
);
}
render() {
- const { error, errorMessage, credentials, viewOptions } = this.props;
+ const { error, errorMessage, credentials, pending, t, viewOptions } = this.props;
const { lastRefresh } = this.state;
+ if (pending) {
+ return this.renderPendingMessage();
+ }
+
if (error) {
return (
-
-
- Error retrieving credentials: {errorMessage}
+
+
+ {t('view.error-message', { context: ['credentials'], message: errorMessage })}
- {this.renderPendingMessage()}
);
}
@@ -304,7 +317,7 @@ class Credentials extends React.Component {
return (
{this.renderPendingMessage()}
- ,
+ ,
);
}
@@ -319,6 +332,7 @@ Credentials.propTypes = {
pending: PropTypes.bool,
credentials: PropTypes.array,
viewOptions: PropTypes.object,
+ t: PropTypes.func,
update: PropTypes.object
};
@@ -331,6 +345,7 @@ Credentials.defaultProps = {
pending: false,
credentials: [],
viewOptions: {},
+ t: translate,
update: {}
};
@@ -341,8 +356,7 @@ const mapDispatchToProps = dispatch => ({
const mapStateToProps = state => ({
...state.credentials.view,
- viewOptions: state.viewOptions[reduxTypes.view.CREDENTIALS_VIEW],
- update: state.credentials.update
+ viewOptions: state.viewOptions[reduxTypes.view.CREDENTIALS_VIEW]
});
const ConnectedCredentials = connect(mapStateToProps, mapDispatchToProps)(Credentials);
diff --git a/src/components/credentials/credentialsEmptyState.js b/src/components/credentials/credentialsEmptyState.js
index b7236a28..57fba9b1 100644
--- a/src/components/credentials/credentialsEmptyState.js
+++ b/src/components/credentials/credentialsEmptyState.js
@@ -1,52 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Button, DropdownButton, EmptyState, Grid, MenuItem, Row } from 'patternfly-react';
+import {
+ Button,
+ ButtonVariant,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ EmptyStatePrimary,
+ EmptyStateSecondaryActions,
+ EmptyStateVariant,
+ Title
+} from '@patternfly/react-core';
+import { AddCircleOIcon } from '@patternfly/react-icons';
+import { AddCredentialType, ButtonVariant as CredentialButtonVariant } from '../addCredentialType/addCredentialType';
import helpers from '../../common/helpers';
+import { translate } from '../i18n/i18n';
-const CredentialsEmptyState = ({ onAddCredential, onAddSource, uiSentenceStartName, uiShortName }) => (
-
-
-
-
- Welcome to {uiShortName}
-
- Credentials contain authentication information needed to scan a source. A credential includes a username
- and a password or SSH key. {uiSentenceStartName} uses SSH to connect to servers on the network and uses
- credentials to access those servers.
-
-
-
- onAddCredential('network')}>
- Network Credential
-
- onAddCredential('satellite')}>
- Satellite Credential
-
- onAddCredential('vcenter')}>
- VCenter Credential
-
-
-
-
-
- Add Source
-
-
-
-
-
+/**
+ * Display an empty state for Credentials.
+ *
+ * @param {object} props
+ * @param {Function} props.onAddSource
+ * @param {Function} props.t
+ * @param {string} props.uiSentenceStartName
+ * @param {string} props.uiShortName
+ * @returns {React.ReactNode}
+ */
+const CredentialsEmptyState = ({ onAddSource, t, uiSentenceStartName, uiShortName }) => (
+
+
+ {t('view.empty-state', { context: 'title', name: uiShortName })}
+
+ {t('view.empty-state', { context: ['description', 'credentials'], name: uiSentenceStartName })}
+
+
+
+
+
+
+ {t('view.empty-state', { context: ['label', 'source'] })}
+
+
+
);
+/**
+ * Prop types
+ *
+ * @type {{uiShortName: string, t: Function, uiSentenceStartName: string, onAddSource: Function}}
+ */
CredentialsEmptyState.propTypes = {
- onAddCredential: PropTypes.func,
onAddSource: PropTypes.func,
+ t: PropTypes.func,
uiSentenceStartName: PropTypes.string,
uiShortName: PropTypes.string
};
+/**
+ * Default props
+ *
+ * @type {{uiShortName: string, t: translate, uiSentenceStartName: string, onAddSource: Function}}
+ */
CredentialsEmptyState.defaultProps = {
- onAddCredential: helpers.noop,
onAddSource: helpers.noop,
+ t: translate,
uiSentenceStartName: helpers.UI_SENTENCE_START_NAME,
uiShortName: helpers.UI_SHORT_NAME
};
diff --git a/src/components/dropdownSelect/__tests__/__snapshots__/dropdownSelect.test.js.snap b/src/components/dropdownSelect/__tests__/__snapshots__/dropdownSelect.test.js.snap
index 30627724..17adfe08 100644
--- a/src/components/dropdownSelect/__tests__/__snapshots__/dropdownSelect.test.js.snap
+++ b/src/components/dropdownSelect/__tests__/__snapshots__/dropdownSelect.test.js.snap
@@ -1,532 +1,687 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`DropdownSelect Component should allow a alternate array and object options: key value object 1`] = `
+exports[`Select Component should allow alternate array and object options: key value object 1`] = `
+Array [
+ Object {
+ "0": "i",
+ "1": "p",
+ "2": "s",
+ "3": "u",
+ "4": "m",
+ "label": "lorem",
+ "selected": true,
+ "text": "lorem",
+ "textContent": "lorem",
+ "title": "lorem",
+ "value": "ipsum",
+ },
+ Object {
+ "0": "w",
+ "1": "o",
+ "2": "r",
+ "3": "l",
+ "4": "d",
+ "label": "hello",
+ "selected": true,
+ "text": "hello",
+ "textContent": "hello",
+ "title": "hello",
+ "value": "world",
+ },
+]
+`;
+
+exports[`Select Component should allow alternate array and object options: key value object 2`] = `
+Array [
+ Object {
+ "label": "lorem",
+ "selected": true,
+ "text": "lorem",
+ "textContent": "lorem",
+ "title": "lorem",
+ "value": "ipsum",
+ },
+ Object {
+ "label": "hello",
+ "selected": true,
+ "text": "hello",
+ "textContent": "hello",
+ "title": "hello",
+ "value": "world",
+ },
+]
+`;
+
+exports[`Select Component should allow alternate array and object options: string array 1`] = `
+Array [
+ Object {
+ "label": "lorem",
+ "selected": false,
+ "text": "lorem",
+ "textContent": "lorem",
+ "title": "lorem",
+ "value": "lorem",
+ },
+ Object {
+ "label": "ipsum",
+ "selected": true,
+ "text": "ipsum",
+ "textContent": "ipsum",
+ "title": "ipsum",
+ "value": "ipsum",
+ },
+ Object {
+ "label": "hello",
+ "selected": false,
+ "text": "hello",
+ "textContent": "hello",
+ "title": "hello",
+ "value": "hello",
+ },
+ Object {
+ "label": "world",
+ "selected": false,
+ "text": "world",
+ "textContent": "world",
+ "title": "world",
+ "value": "world",
+ },
+]
+`;
+
+exports[`Select Component should allow alternate array and object options: undefined options 1`] = `Array []`;
+
+exports[`Select Component should allow alternate direction and position options: direction up 1`] = `
+Object {
+ "className": "quipucords-select-pf ",
+ "direction": "up",
+}
+`;
+
+exports[`Select Component should allow alternate direction and position options: position right 1`] = `
+Object {
+ "className": "quipucords-select-pf quipucords-select-pf__position-down quipucords-select-pf__position-right ",
+ "direction": "down",
+}
+`;
+
+exports[`Select Component should allow being disabled with missing options: no options 1`] = `
-
-
- Select option
-
-
-
-
-
+
`;
-exports[`DropdownSelect Component should allow a alternate array and object options: string array 1`] = `
+exports[`Select Component should allow being disabled with missing options: options, but disabled 1`] = `
-
-
- ipsum
-
-
-
-
-
+`;
+
+exports[`Select Component should allow being disabled with missing options: options, but no content 1`] = `
+
+
+
+`;
+
+exports[`Select Component should allow data- props: data- attributes 1`] = `
+Object {
+ "ariaLabel": "Select option",
+ "buttonVariant": "default",
+ "className": "",
+ "data-dolor-sit": "dolor sit",
+ "data-lorem": "ipsum",
+ "direction": "down",
+ "id": "generatedid-",
+ "isDisabled": false,
+ "isDropdownButton": false,
+ "isInline": true,
+ "isToggleText": true,
+ "maxHeight": null,
+ "name": null,
+ "onSelect": [Function],
+ "options": Array [],
+ "placeholder": "Select option",
+ "position": "left",
+ "selectedOptions": null,
+ "splitButtonVariant": null,
+ "toggleIcon": null,
+ "variant": "single",
+}
+`;
+
+exports[`Select Component should allow plain objects as values, and be able to select options based on values within the object: select when option values are objects 1`] = `
+Array [
+ Object {
+ "label": "lorem",
+ "selected": false,
+ "text": "lorem",
+ "textContent": "lorem",
+ "title": "lorem",
+ "value": Object {
+ "dolor": "sit",
+ },
+ },
+ Object {
+ "label": "dolor",
+ "selected": false,
+ "text": "dolor",
+ "textContent": "dolor",
+ "title": "dolor",
+ "value": Object {
+ "lorem": "ipsum",
+ },
+ },
+ Object {
+ "label": "hello",
+ "selected": true,
+ "text": "hello",
+ "textContent": "hello",
+ "title": "hello",
+ "value": Object {
+ "hello": "world",
+ },
+ },
+]
+`;
+
+exports[`Select Component should allow selected options to match value or title: value or title match 1`] = `
+Array [
+ Object {
+ "0": "i",
+ "1": "p",
+ "2": "s",
+ "3": "u",
+ "4": "m",
+ "label": "lorem",
+ "selected": true,
+ "text": "lorem",
+ "textContent": "lorem",
+ "title": "lorem",
+ "value": "ipsum",
+ },
+ Object {
+ "0": "w",
+ "1": "o",
+ "2": "r",
+ "3": "l",
+ "4": "d",
+ "label": "hello",
+ "selected": true,
+ "text": "hello",
+ "textContent": "hello",
+ "title": "hello",
+ "value": "world",
+ },
+ Object {
+ "0": "s",
+ "1": "e",
+ "2": "t",
+ "label": "dolor",
+ "selected": false,
+ "text": "dolor",
+ "textContent": "dolor",
+ "title": "dolor",
+ "value": "set",
+ },
+]
+`;
+
+exports[`Select Component should apply patternfly dropdown props based on wrapper props: dropdown props, button variants 1`] = `
+Object {
+ "isPlain": true,
+ "splitButtonVariant": "checkbox",
+ "toggleIndicator": null,
+}
+`;
+
+exports[`Select Component should apply patternfly dropdown props based on wrapper props: dropdown props, disabled 1`] = `
+Object {
+ "isDisabled": true,
+}
+`;
+
+exports[`Select Component should apply patternfly dropdown props based on wrapper props: dropdown props, no options, disabled 1`] = `
+Object {
+ "isDisabled": true,
+}
+`;
+
+exports[`Select Component should apply patternfly dropdown props based on wrapper props: dropdown props, options, disabled 1`] = `
+Object {
+ "isDisabled": true,
+}
+`;
+
+exports[`Select Component should apply patternfly dropdown props based on wrapper props: dropdown props, placeholder 1`] = `Object {}`;
+
+exports[`Select Component should apply patternfly select props based on wrapper props: select props, disabled 1`] = `
+Object {
+ "isDisabled": true,
+}
+`;
+
+exports[`Select Component should apply patternfly select props based on wrapper props: select props, no options, disabled 1`] = `
+Object {
+ "isDisabled": true,
+}
+`;
+
+exports[`Select Component should apply patternfly select props based on wrapper props: select props, options, disabled 1`] = `
+Object {
+ "isDisabled": true,
+}
+`;
+
+exports[`Select Component should apply patternfly select props based on wrapper props: select props, placeholder 1`] = `
+Object {
+ "hasPlaceholderStyle": true,
+}
+`;
+
+exports[`Select Component should disable toggle text: disabled text 1`] = `"quipucords-select-pf__no-toggle-text quipucords-select-pf__position-down "`;
+
+exports[`Select Component should emulate pf dropdown: emulated dropdown 1`] = `
+
+
+
-
- world
-
-
-
+
+ Select option
+
+
+
+
+
+
+
+
+
`;
-exports[`DropdownSelect Component should render a basic component: basic dropdown 1`] = `
+exports[`Select Component should render a basic component: basic component 1`] = `
-
-
- hello
-
-
-
-
-
+
- hello
-
-
-
+
+
+
+
+
+
`;
-exports[`DropdownSelect Component should render a basic component: multiselect dropdown 1`] = `
+exports[`Select Component should render a checkbox select: checkbox select 1`] = `
-
-
- lorem,hello
-
-
-
-
-
+
+
+
+
+
`;
-exports[`DropdownSelect Component should return an emulated onchange event: emulated event 1`] = `
+exports[`Select Component should render an expanded select: expanded 1`] = `
+Array [
+
+ lorem
+ ,
+
+ ipsum
+ ,
+
+ hello
+ ,
+
+ world
+ ,
+]
+`;
+
+exports[`Select Component should return an emulated onchange event, checklist variant: checklist emulated event 1`] = `
Object {
- "currentTarget": Object {
- "id": "test",
- "name": "test",
- "options": Array [
- Object {
- "label": "lorem",
- "selected": false,
- "text": "lorem",
- "textContent": "lorem",
- "title": "lorem",
- "value": "lorem",
- },
- Object {
- "label": "ipsum",
- "selected": false,
- "text": "ipsum",
- "textContent": "ipsum",
- "title": "ipsum",
- "value": "ipsum",
- },
- Object {
- "label": "hello",
- "selected": true,
- "text": "hello",
- "textContent": "hello",
- "title": "hello",
- "value": "hello",
- },
- Object {
- "label": "world",
- "selected": false,
- "text": "world",
- "textContent": "world",
- "title": "world",
- "value": "world",
- },
- ],
- "selectedIndex": 2,
- "type": "select-one",
- "value": "hello",
- },
+ "checked": true,
"id": "test",
"name": "test",
- "options": Array [
- Object {
- "label": "lorem",
- "selected": false,
- "text": "lorem",
- "textContent": "lorem",
- "title": "lorem",
- "value": "lorem",
- },
- Object {
- "label": "ipsum",
- "selected": false,
- "text": "ipsum",
- "textContent": "ipsum",
- "title": "ipsum",
- "value": "ipsum",
- },
- Object {
- "label": "hello",
- "selected": true,
- "text": "hello",
- "textContent": "hello",
- "title": "hello",
- "value": "hello",
- },
- Object {
- "label": "world",
- "selected": false,
- "text": "world",
- "textContent": "world",
- "title": "world",
- "value": "world",
- },
- ],
"persist": [Function],
+ "selected": Array [
+ "ipsum",
+ "hello",
+ "world",
+ ],
"selectedIndex": 2,
- "target": Object {
- "id": "test",
- "name": "test",
- "options": Array [
- Object {
- "label": "lorem",
- "selected": false,
- "text": "lorem",
- "textContent": "lorem",
- "title": "lorem",
- "value": "lorem",
- },
- Object {
- "label": "ipsum",
- "selected": false,
- "text": "ipsum",
- "textContent": "ipsum",
- "title": "ipsum",
- "value": "ipsum",
- },
- Object {
- "label": "hello",
- "selected": true,
- "text": "hello",
- "textContent": "hello",
- "title": "hello",
- "value": "hello",
- },
- Object {
- "label": "world",
- "selected": false,
- "text": "world",
- "textContent": "world",
- "title": "world",
- "value": "world",
- },
- ],
- "selectedIndex": 2,
- "type": "select-one",
+ "type": "select-multiple",
+ "value": "hello",
+}
+`;
+
+exports[`Select Component should return an emulated onchange event: default emulated event 1`] = `
+Object {
+ "id": "test",
+ "name": "test",
+ "persist": [Function],
+ "selected": Object {
+ "label": "hello",
+ "selected": true,
+ "text": "hello",
+ "textContent": "hello",
+ "title": "hello",
"value": "hello",
},
+ "selectedIndex": 2,
"type": "select-one",
"value": "hello",
}
`;
-
-exports[`DropdownSelect Component should return an emulated onchange event: key value object 1`] = `
-
-
-
- Select option
-
-
-
-
-
-
-`;
-
-exports[`DropdownSelect Component should return an emulated onchange event: string array 1`] = `
-
-
-
- hello
-
-
-
-
-
-
-`;
diff --git a/src/components/dropdownSelect/__tests__/dropdownSelect.test.js b/src/components/dropdownSelect/__tests__/dropdownSelect.test.js
index ed9868fd..b42a8a2c 100644
--- a/src/components/dropdownSelect/__tests__/dropdownSelect.test.js
+++ b/src/components/dropdownSelect/__tests__/dropdownSelect.test.js
@@ -1,10 +1,19 @@
import React from 'react';
-import { mount } from 'enzyme';
-import { MenuItem } from 'patternfly-react';
-import DropdownSelect from '../dropdownSelect';
+import { SelectVariant } from '@patternfly/react-core';
+import { FilterIcon } from '@patternfly/react-icons';
+import {
+ ButtonVariant,
+ DropdownSelect,
+ formatOptions,
+ formatButtonProps,
+ formatSelectProps,
+ SelectDirection,
+ SelectPosition,
+ SplitButtonVariant
+} from '../dropdownSelect';
-describe('DropdownSelect Component', () => {
- it('should render a basic component', () => {
+describe('Select Component', () => {
+ it('should render a basic component', async () => {
const props = {
id: 'test',
options: [
@@ -13,83 +22,264 @@ describe('DropdownSelect Component', () => {
]
};
- let component = mount(
-
- Lorem ipsum
-
- );
+ const component = await mountHookWrapper( );
+ expect(component.render()).toMatchSnapshot('basic component');
+ });
+
+ it('should render a checkbox select', async () => {
+ const props = {
+ id: 'test',
+ options: [
+ { title: 'lorem', value: 'ipsum' },
+ { title: 'hello', value: 'world', selected: true }
+ ],
+ selectedOptions: ['world', 'ipsum'],
+ variant: SelectVariant.checkbox,
+ placeholder: 'multiselect test'
+ };
+
+ const component = await mountHookWrapper( );
+ expect(component.render()).toMatchSnapshot('checkbox select');
+ });
+
+ it('should apply patternfly select props based on wrapper props', () => {
+ const props = {};
- expect(component.render()).toMatchSnapshot('basic dropdown');
+ expect(formatSelectProps(props)).toMatchSnapshot('select props, disabled');
- props.selectValue = ['world', 'ipsum'];
- props.multiselect = true;
+ props.options = [];
+ expect(formatSelectProps(props)).toMatchSnapshot('select props, no options, disabled');
- component = mount(
-
- Lorem ipsum
-
- );
+ props.options = ['lorem', 'ipsum'];
+ props.isDisabled = true;
+ expect(formatSelectProps(props)).toMatchSnapshot('select props, options, disabled');
- expect(component.render()).toMatchSnapshot('multiselect dropdown');
+ props.placeholder = 'dolor sit';
+ props.isDisabled = false;
+ expect(formatSelectProps(props)).toMatchSnapshot('select props, placeholder');
});
- it('should allow a alternate array and object options', () => {
+ it('should apply patternfly dropdown props based on wrapper props', () => {
+ const props = {};
+
+ expect(formatButtonProps(props)).toMatchSnapshot('dropdown props, disabled');
+
+ props.options = [];
+ expect(formatButtonProps(props)).toMatchSnapshot('dropdown props, no options, disabled');
+
+ props.options = ['lorem', 'ipsum'];
+ props.isDisabled = true;
+ expect(formatButtonProps(props)).toMatchSnapshot('dropdown props, options, disabled');
+
+ props.placeholder = 'dolor sit';
+ props.isDisabled = false;
+ expect(formatButtonProps(props)).toMatchSnapshot('dropdown props, placeholder');
+
+ props.buttonVariant = ButtonVariant.plain;
+ props.splitButtonVariant = SplitButtonVariant.checkbox;
+ expect(formatButtonProps(props)).toMatchSnapshot('dropdown props, button variants');
+ });
+
+ it('should allow alternate array and object options', async () => {
const props = {
- id: 'test',
options: ['lorem', 'ipsum', 'hello', 'world'],
- selectValue: ['ipsum']
+ selectedOptions: ['ipsum']
};
- let component = mount(
-
- Lorem ipsum
-
- );
-
- expect(component.render()).toMatchSnapshot('string array');
+ expect(formatOptions(props).options).toMatchSnapshot('string array');
props.options = { lorem: 'ipsum', hello: 'world' };
+ props.selectedOptions = ['world', 'ipsum'];
+
+ expect(formatOptions(props).options).toMatchSnapshot('key value object');
+
+ props.options = [
+ { title: 'lorem', value: 'ipsum' },
+ { title: () => 'hello', value: 'world' }
+ ];
+ props.selectedOptions = ['world', 'ipsum'];
+
+ expect(formatOptions(props).options).toMatchSnapshot('key value object');
+
+ props.options = undefined;
+ props.selectedOptions = [];
+
+ expect(formatOptions(props).options).toMatchSnapshot('undefined options');
+ });
+
+ it('should allow plain objects as values, and be able to select options based on values within the object', async () => {
+ const props = {
+ options: [
+ { title: 'lorem', value: { dolor: 'sit' } },
+ { title: 'dolor', value: { lorem: 'ipsum' } },
+ { title: 'hello', value: { hello: 'world' } }
+ ],
+ selectedOptions: ['world']
+ };
+
+ expect(formatOptions(props).options).toMatchSnapshot('select when option values are objects');
+ });
+
+ it('should allow selected options to match value or title', async () => {
+ const props = {
+ options: { lorem: 'ipsum', hello: 'world', dolor: 'set' },
+ selectedOptions: ['world', 'lorem', 'fail'],
+ variant: SelectVariant.checkbox
+ };
+
+ expect(formatOptions(props).options).toMatchSnapshot('value or title match');
+ });
+
+ it('should return an emulated onchange event', async () => {
+ const mockOnSelect = jest.fn();
+ const props = {
+ id: 'test',
+ options: ['lorem', 'ipsum', 'hello', 'world'],
+ selectedOptions: ['ipsum'],
+ onSelect: mockOnSelect
+ };
- component = mount(
-
- Lorem ipsum
-
- );
+ const component = await mountHookWrapper( , {
+ callback: ({ component: comp }) => {
+ comp.find('button').simulate('click');
+ }
+ });
- expect(component.render()).toMatchSnapshot('key value object');
+ component.find('ul.pf-c-select__menu').find('button').at(2).simulate('click');
+
+ expect(mockOnSelect).toHaveBeenCalledTimes(1);
+
+ const { currentTarget, options, target, ...rest } = mockOnSelect.mock.calls[0][0];
+ expect(rest).toMatchSnapshot('default emulated event');
+ mockOnSelect.mockClear();
});
- it('should return an emulated onchange event', done => {
+ it('should return an emulated onchange event, checklist variant', async () => {
+ const mockOnSelect = jest.fn();
const props = {
id: 'test',
options: ['lorem', 'ipsum', 'hello', 'world'],
- selectValue: ['ipsum']
+ selectedOptions: ['ipsum'],
+ onSelect: mockOnSelect,
+ variant: SelectVariant.checkbox
};
- props.onSelect = event => {
- expect(event).toMatchSnapshot('emulated event');
- done();
+ const component = await mountHookWrapper( , {
+ callback: ({ component: comp }) => {
+ comp.find('button').simulate('click');
+ }
+ });
+
+ component
+ .find('ul.pf-c-select__menu input.pf-c-check__input')
+ .at(3)
+ .simulate('change', { target: { checked: true } });
+
+ expect(mockOnSelect).toHaveBeenCalledTimes(1);
+
+ component
+ .find('ul.pf-c-select__menu input.pf-c-check__input')
+ .at(2)
+ .simulate('change', { target: { checked: true } });
+
+ expect(mockOnSelect).toHaveBeenCalledTimes(2);
+
+ const { currentTarget, options, target, ...rest } = mockOnSelect.mock.calls[1][0];
+ expect(rest).toMatchSnapshot('checklist emulated event');
+ mockOnSelect.mockClear();
+ });
+
+ it('should render an expanded select', async () => {
+ const props = {
+ id: 'test',
+ options: ['lorem', 'ipsum', 'hello', 'world']
};
- let component = mount(
-
- Lorem ipsum
-
- );
+ const component = await mountHookWrapper( , {
+ callback: ({ component: comp }) => {
+ comp.find('button').simulate('click');
+ }
+ });
- const componentInstance = component.instance();
- componentInstance.onSelect('hello');
+ expect(component.find('ul.pf-c-select__menu').find('button')).toMatchSnapshot('expanded');
+ });
- expect(component.render()).toMatchSnapshot('string array');
+ it('should disable toggle text', async () => {
+ const props = {
+ id: 'test',
+ options: ['lorem', 'ipsum'],
+ toggleIcon: ,
+ isToggleText: false
+ };
- props.options = { lorem: 'ipsum', hello: 'world' };
+ const component = await shallowHookWrapper( );
+ expect(component.find('.quipucords-select-pf__no-toggle-text').props().className).toMatchSnapshot('disabled text');
+ });
+
+ it('should allow alternate direction and position options', async () => {
+ const props = {
+ id: 'test',
+ options: ['lorem', 'ipsum'],
+ direction: SelectDirection.up
+ };
- component = mount(
-
- Lorem ipsum
-
- );
+ const component = await shallowHookWrapper( );
+ const upLeftProps = component.find('.quipucords-select-pf').props();
+ expect({
+ direction: upLeftProps.direction,
+ className: upLeftProps.className
+ }).toMatchSnapshot('direction up');
+
+ component.setProps({ direction: SelectDirection.down, position: SelectPosition.right });
+ const downRightProps = component.find('.quipucords-select-pf').props();
+ expect({
+ direction: downRightProps.direction,
+ className: downRightProps.className
+ }).toMatchSnapshot('position right');
+ });
+
+ it('should allow being disabled with missing options', async () => {
+ const props = {
+ id: 'test',
+ options: undefined
+ };
+
+ const component = await shallowHookWrapper( );
+ expect(component).toMatchSnapshot('no options');
+
+ component.setProps({
+ options: [],
+ isDisabled: false
+ });
+
+ expect(component).toMatchSnapshot('options, but no content');
+
+ component.setProps({
+ options: ['lorem', 'ipsum', 'hello', 'world'],
+ isDisabled: true
+ });
+
+ expect(component).toMatchSnapshot('options, but disabled');
+ });
+
+ it('should allow data- props', async () => {
+ const props = {
+ 'data-lorem': 'ipsum',
+ 'data-dolor-sit': 'dolor sit'
+ };
+
+ const component = await mountHookWrapper( );
+ expect(component.props()).toMatchSnapshot('data- attributes');
+ });
+
+ it('should emulate pf dropdown', async () => {
+ const props = {
+ isDropdownButton: true,
+ buttonVariant: ButtonVariant.secondary,
+ options: ['lorem', 'ipsum', 'hello', 'world']
+ };
- expect(component.render()).toMatchSnapshot('key value object');
+ const component = await mountHookWrapper( );
+ expect(component.render()).toMatchSnapshot('emulated dropdown');
});
});
diff --git a/src/components/dropdownSelect/dropdownSelect.js b/src/components/dropdownSelect/dropdownSelect.js
index 102024dc..f29d405e 100644
--- a/src/components/dropdownSelect/dropdownSelect.js
+++ b/src/components/dropdownSelect/dropdownSelect.js
@@ -1,209 +1,557 @@
-import React from 'react';
+import React, { useRef, useState } from 'react';
import PropTypes from 'prop-types';
-import { Dropdown, Grid, Icon, MenuItem } from 'patternfly-react';
-import _isPlainObject from 'lodash/isPlainObject';
+import { useShallowCompareEffect } from 'react-use';
+import {
+ ButtonVariant as PfButtonVariant,
+ Dropdown,
+ DropdownDirection,
+ DropdownItem,
+ DropdownPosition,
+ DropdownToggle,
+ Select as PfSelect,
+ SelectOption as PfSelectOption,
+ SelectVariant
+} from '@patternfly/react-core';
import _cloneDeep from 'lodash/cloneDeep';
-import helpers from '../../common/helpers';
+import _findIndex from 'lodash/findIndex';
+import _isPlainObject from 'lodash/isPlainObject';
+import { helpers } from '../../common';
+
+/**
+ * Dropdown split button variants
+ *
+ * @type {{checkbox: string, action: string}}
+ */
+const SplitButtonVariant = {
+ action: 'action',
+ checkbox: 'checkbox'
+};
+
+/**
+ * Dropdown toggle button variants
+ *
+ * @type {{secondary: string, default: string, plain: string, text: string, primary: string}}
+ */
+const ButtonVariant = {
+ default: 'default',
+ plain: 'plain',
+ primary: 'primary',
+ secondary: 'secondary',
+ text: 'text'
+};
+
+/**
+ * Pass button variant as a select component option.
+ *
+ * @type {PfButtonVariant}
+ */
+const SelectButtonVariant = PfButtonVariant;
+
+/**
+ * Pass direction as select component variant option.
+ *
+ * @type {DropdownDirection}
+ */
+const SelectDirection = DropdownDirection;
+
+/**
+ * Pass position as select component variant option.
+ *
+ * @type {DropdownPosition}
+ */
+const SelectPosition = DropdownPosition;
+
+// FixMe: attributes filtered on PF select component. allow data- attributes
+/**
+ * Format options into a consumable array of objects format.
+ * Note: It is understood that for line 83'ish around "updatedOptions" we dump all values regardless
+ * of whether they are plain objects, or not, into updatedOptions. This has been done for speed only,
+ * one less check to perform.
+ *
+ * @param {object} params
+ * @param {*|React.ReactNode} params.selectField
+ * @param {object|Array} params.options
+ * @param {string|number|Array} params.selectedOptions
+ * @param {string} params.variant
+ * @param {object} params.props
+ * @returns {{options: *[]|*, selected: *[]}}
+ */
+const formatOptions = ({ selectField = { current: null }, options, selectedOptions, variant, ...props } = {}) => {
+ const { current: domElement = {} } = selectField;
+ const dataAttributes = Object.entries(props).filter(([key]) => /^data-/i.test(key));
+ const updatedOptions =
+ (_isPlainObject(options) && Object.entries(options).map(([key, value]) => ({ ...value, title: key, value }))) ||
+ (options && _cloneDeep(options)) ||
+ [];
+ const isSelectedOptionsStringNumber = typeof selectedOptions === 'string' || typeof selectedOptions === 'number';
+ const activateOptions =
+ (Array.isArray(selectedOptions) && selectedOptions) || (isSelectedOptionsStringNumber && [selectedOptions]) || [];
+
+ updatedOptions.forEach((option, index) => {
+ let convertedOption = option;
+
+ if (typeof convertedOption === 'string') {
+ convertedOption = {
+ title: option,
+ value: option
+ };
+
+ updatedOptions[index] = convertedOption;
+ } else if (typeof convertedOption.title === 'function') {
+ convertedOption.title = convertedOption.title();
+ }
+
+ convertedOption.text = convertedOption.text || convertedOption.title;
+ convertedOption.textContent = convertedOption.textContent || convertedOption.title;
+ convertedOption.label = convertedOption.label || convertedOption.title;
-class DropdownSelect extends React.Component {
- static filterTitle(options, multiselect) {
- const title = [];
+ if (activateOptions.length) {
+ let isSelected;
- for (let i = 0; i < options.length; i++) {
- if (options[i].selected === true && options[i].title) {
- title.push(options[i].title);
+ if (_isPlainObject(convertedOption.value)) {
+ isSelected = _findIndex(activateOptions, convertedOption.value) > -1;
+
+ if (!isSelected) {
+ const tempSearch = activateOptions.find(activeOption =>
+ Object.values(convertedOption.value).includes(activeOption)
+ );
+ isSelected = !!tempSearch;
+ }
+ } else {
+ isSelected = activateOptions.includes(convertedOption.value);
}
- if (!multiselect && title.length > 0) {
- break;
+ if (!isSelected) {
+ isSelected = activateOptions.includes(convertedOption.title);
}
- }
- return title.length ? title.join(',') : null;
- }
+ updatedOptions[index].selected = isSelected;
+ }
+ });
- state = {
- isOpen: false,
- options: null,
- selectedTitle: []
- };
+ let updateSelected;
- componentDidMount() {
- const { options } = this.state;
+ if (variant === SelectVariant.single) {
+ updateSelected = (updatedOptions.find(opt => opt.selected === true) || {}).title;
+ } else {
+ updateSelected = updatedOptions.filter(opt => opt.selected === true).map(opt => opt.title);
+ }
- if (options === null) {
- this.formatFilterOptions();
- }
+ if (domElement?.parentRef?.current) {
+ dataAttributes.forEach(([key, value]) => domElement?.parentRef?.current.setAttribute(key, value));
}
- onSelect = value => {
- const { options } = this.state;
- const { id, name, multiselect, onSelect } = this.props;
- const updatedOptions = _cloneDeep(options);
+ return {
+ options: updatedOptions,
+ selected: updateSelected
+ };
+};
- const optionsIndex = updatedOptions.findIndex(option => option.value === value);
+/**
+ * Return assumed/expected PF select props.
+ *
+ * @param {object} params
+ * @param {boolean} params.isDisabled
+ * @param {string} params.placeholder
+ * @param {object|Array} params.options
+ * @returns {{}}
+ */
+const formatSelectProps = ({ isDisabled, placeholder, options } = {}) => {
+ const updatedProps = {};
+
+ if (!options || !options.length || isDisabled) {
+ updatedProps.isDisabled = true;
+ }
- updatedOptions[optionsIndex].selected = !multiselect ? true : !updatedOptions[optionsIndex].selected;
+ if (typeof placeholder === 'string' && placeholder) {
+ updatedProps.hasPlaceholderStyle = true;
+ }
- if (!multiselect) {
- updatedOptions.forEach((option, index) => {
- if (optionsIndex !== index) {
- updatedOptions[index].selected = false;
- }
- });
- }
+ return updatedProps;
+};
- const updatedSelectedTitle = DropdownSelect.filterTitle(updatedOptions, multiselect);
-
- this.setState(
- {
- selectedTitle: updatedSelectedTitle,
- options: updatedOptions
- },
- () => {
- const mockTarget = {
- id,
- name: name || id,
- value: updatedOptions[optionsIndex].value,
- selectedIndex: optionsIndex,
- type: `select-${(multiselect && 'multiple') || 'one'}`,
- options: updatedOptions
- };
- const mockEvent = {
- ...mockTarget,
- target: { ...mockTarget },
- currentTarget: { ...mockTarget },
- persist: helpers.noop
- };
-
- onSelect({ ...mockEvent }, optionsIndex, updatedOptions);
- }
- );
+/**
+ * Format consistent dropdown button props.
+ *
+ * @param {object} params
+ * @param {boolean} params.isDisabled
+ * @param {Array} params.options
+ * @param {string} params.buttonVariant
+ * @param {string} params.splitButtonVariant
+ * @returns {*}
+ */
+const formatButtonProps = ({ isDisabled, options, buttonVariant, splitButtonVariant } = {}) => {
+ const buttonVariantPropLookup = {
+ default: { toggleVariant: 'default' },
+ plain: { isPlain: true, toggleIndicator: null },
+ primary: { toggleVariant: 'primary' },
+ secondary: { toggleVariant: 'secondary' },
+ text: { isText: true }
};
- onToggleDropDown = (a, b, c) => {
- const { isOpen } = this.state;
+ const splitButtonVariantPropLookup = {
+ action: { splitButtonVariant: 'action' },
+ checkbox: { splitButtonVariant: 'checkbox' }
+ };
- this.setState({
- isOpen: (c && c.source === 'select') || !isOpen
- });
+ const updatedProps = {
+ ...buttonVariantPropLookup[buttonVariant],
+ ...splitButtonVariantPropLookup[splitButtonVariant]
};
- formatFilterOptions() {
- const { options, multiselect, selectValue } = this.props;
- const updatedOptions = _isPlainObject(options)
- ? Object.keys(options).map(value => ({ title: options[value], value }))
- : _cloneDeep(options);
+ if (!options || !options.length || isDisabled) {
+ updatedProps.isDisabled = true;
+ }
- const updatedTitle = [];
- const activateValues =
- (selectValue && typeof selectValue === 'string') || typeof selectValue === 'number' ? [selectValue] : selectValue;
+ return updatedProps;
+};
- updatedOptions.forEach((option, index) => {
- let convertedOption = option;
+/**
+ * FixMe: PF has an inconsistency in how it applies props for the dropdown
+ * Sometimes those props are on the toggle, sometimes those props are on the parent, little bit of guesswork.
+ * Additionally, it's not filtering props so you'll throw the "[HTML doesn't recognize attribute]" error.
+ */
+/**
+ * Fix pf props inconsistency for dropdown button props.
+ *
+ * @param {object} formattedButtonProps
+ * @returns {*}
+ */
+const formatButtonParentProps = (formattedButtonProps = {}) => {
+ const updatedButtonProps = formatButtonProps(formattedButtonProps);
+ delete updatedButtonProps.isDisabled;
+ delete updatedButtonProps.toggleIndicator;
+
+ return updatedButtonProps;
+};
- if (typeof convertedOption === 'string') {
- convertedOption = {
- title: option,
- value: option
- };
+/**
+ * A wrapper for Pf Select, and emulator for Pf Dropdown. Provides consistent restructured event data for onSelect callback
+ * for both select and dropdown.
+ *
+ * @fires onDropdownSelect
+ * @fires onToggle
+ * @param {object} props
+ * @param {string} props.ariaLabel
+ * @param {string} props.buttonVariant
+ * @param {string} props.className
+ * @param {string} props.direction
+ * @param {string} props.id
+ * @param {boolean} props.isDisabled
+ * @param {boolean} props.isDropdownButton
+ * @param {boolean} props.isInline
+ * @param {boolean} props.isToggleText
+ * @param {number} props.maxHeight
+ * @param {string} props.name
+ * @param {Function} props.onSelect
+ * @param {object|Array} props.options
+ * @param {string} props.placeholder
+ * @param {string} props.position
+ * @param {number|string|Array} props.selectedOptions
+ * @param {string} props.splitButtonVariant
+ * @param {React.ReactNode|Function} props.toggleIcon
+ * @param {string} props.variant
+ * @param {object} props.props
+ * @returns {React.ReactNode}
+ */
+const DropdownSelect = ({
+ ariaLabel,
+ buttonVariant,
+ className,
+ direction,
+ id,
+ isDisabled,
+ isDropdownButton,
+ isInline,
+ isToggleText,
+ maxHeight,
+ name,
+ onSelect,
+ options: baseOptions,
+ placeholder,
+ position,
+ selectedOptions,
+ splitButtonVariant,
+ toggleIcon,
+ variant,
+ ...props
+}) => {
+ const [isExpanded, setIsExpanded] = useState(false);
+ const [options, setOptions] = useState(baseOptions);
+ const [selected, setSelected] = useState([]);
+ const selectField = useRef();
+
+ useShallowCompareEffect(() => {
+ const { options: updatedOptions, selected: updatedSelected } = formatOptions({
+ selectField,
+ options: baseOptions,
+ selectedOptions,
+ variant,
+ ...props
+ });
- updatedOptions[index] = convertedOption;
- }
+ setOptions(updatedOptions);
+ setSelected(updatedSelected);
+ }, [baseOptions, props, selectField, selectedOptions, variant]);
+
+ /**
+ * Open/closed state.
+ *
+ * @event onToggle
+ * @param {boolean} expanded
+ */
+ const onToggle = expanded => {
+ setIsExpanded(expanded);
+ };
- convertedOption.text = convertedOption.text || convertedOption.title;
- convertedOption.textContent = convertedOption.textContent || convertedOption.title;
- convertedOption.label = convertedOption.label || convertedOption.title;
+ /**
+ * Emulate select event object, apply to provided onSelect prop.
+ *
+ * @event onDropdownSelect
+ * @param {object} event
+ * @param {string} titleSelection
+ */
+ const onDropdownSelect = (event, titleSelection) => {
+ const updatedOptions = options;
+ const optionsIndex = updatedOptions.findIndex(
+ option =>
+ (titleSelection && option.title === titleSelection) ||
+ event.currentTarget.querySelector('[data-title]')?.getAttribute('data-title') === option.title ||
+ event.currentTarget.innerText === option.title
+ );
- if (activateValues) {
- updatedOptions[index].selected = activateValues.includes(convertedOption.value);
- }
+ updatedOptions[optionsIndex].selected =
+ variant === SelectVariant.single ? true : !updatedOptions[optionsIndex].selected;
- if (convertedOption.selected === true) {
- if (!multiselect && updatedTitle.length) {
+ if (variant === SelectVariant.single) {
+ updatedOptions.forEach((option, index) => {
+ if (optionsIndex !== index) {
updatedOptions[index].selected = false;
}
+ });
+ }
- if (multiselect || (!multiselect && !updatedTitle.length)) {
- updatedTitle.push(convertedOption.title);
- }
- }
- });
+ const updateSelected =
+ variant === SelectVariant.single
+ ? titleSelection
+ : updatedOptions.filter(opt => opt.selected === true).map(opt => opt.title);
+
+ const mockUpdatedOptions = _cloneDeep(updatedOptions);
+
+ const mockTarget = {
+ id,
+ name: name || id,
+ value: mockUpdatedOptions[optionsIndex].value,
+ selected: (variant === SelectVariant.single && mockUpdatedOptions[optionsIndex]) || _cloneDeep(updateSelected),
+ selectedIndex: optionsIndex,
+ type: `select-${(variant === SelectVariant.single && 'one') || 'multiple'}`,
+ options: mockUpdatedOptions
+ };
+
+ if (variant === SelectVariant.checkbox) {
+ mockTarget.checked = mockUpdatedOptions[optionsIndex].selected;
+ }
- this.setState({
- selectedTitle: updatedTitle.length ? updatedTitle.join(',') : null,
- options: updatedOptions
- });
- }
+ const mockEvent = {
+ ...mockTarget,
+ target: { ...mockTarget },
+ currentTarget: { ...mockTarget },
+ persist: helpers.noop
+ };
+
+ setOptions(updatedOptions);
+ setSelected(updateSelected);
- render() {
- const { isOpen, options, selectedTitle } = this.state;
- const { className, id, name, multiselect, pullRight, title } = this.props;
- const additionalProps = {};
+ onSelect({ ...mockEvent }, optionsIndex, mockUpdatedOptions);
- if (multiselect) {
- additionalProps.onToggle = this.onToggleDropDown;
- additionalProps.open = isOpen;
+ if (variant === SelectVariant.single) {
+ setIsExpanded(false);
}
+ };
- return (
+ /**
+ * Apply dropdown.
+ *
+ * @returns {React.ReactNode}
+ */
+ const renderDropdownButton = () => (
+
-
- {selectedTitle || title}
-
-
- {options &&
- options.map(option => (
-
- {!multiselect && option.title}
- {multiselect && (
-
-
- {option.title}
-
-
- {option.selected && }
-
-
- )}
-
- ))}
-
-
- );
- }
-}
+ direction={direction}
+ isOpen={isExpanded}
+ position={position}
+ toggle={
+
+ {placeholder || ariaLabel}
+
+ }
+ dropdownItems={
+ options?.map(option => (
+
+ {option.title}
+
+ )) || []
+ }
+ {...formatButtonParentProps({ buttonVariant })}
+ {...props}
+ />
+
+ );
+
+ /**
+ * Apply select.
+ *
+ * @returns {React.ReactNode}
+ */
+ const renderSelect = () => (
+
+ {options?.map(option => (
+
+ )) || []}
+
+ );
+
+ return (
+
+ {(isDropdownButton && renderDropdownButton()) || renderSelect()}
+
+ );
+};
+/**
+ * Prop types.
+ *
+ * @type {{toggleIcon: (React.ReactNode|Function), className: string, ariaLabel: string, onSelect: Function, isToggleText: boolean,
+ * isDropdownButton: boolean, maxHeight: number, buttonVariant: string, name: string, options: Array|object,
+ * selectedOptions: Array|number|string, isInline: boolean, id: string, isDisabled: boolean, placeholder: string,
+ * position: string, splitButtonVariant: string, direction: string}}
+ */
DropdownSelect.propTypes = {
+ ariaLabel: PropTypes.string,
+ buttonVariant: PropTypes.oneOf(Object.values(ButtonVariant)),
className: PropTypes.string,
+ direction: PropTypes.oneOf(Object.values(SelectDirection)),
id: PropTypes.string,
- multiselect: PropTypes.bool,
+ isDisabled: PropTypes.bool,
+ isDropdownButton: PropTypes.bool,
+ isInline: PropTypes.bool,
+ isToggleText: PropTypes.bool,
+ maxHeight: PropTypes.number,
name: PropTypes.string,
onSelect: PropTypes.func,
- options: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
- pullRight: PropTypes.bool,
- selectValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.array]),
- title: PropTypes.string
+ options: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.string),
+ PropTypes.arrayOf(
+ PropTypes.shape({
+ description: PropTypes.any,
+ selected: PropTypes.bool,
+ title: PropTypes.any,
+ value: PropTypes.any.isRequired
+ })
+ ),
+ PropTypes.shape({
+ description: PropTypes.any,
+ selected: PropTypes.bool,
+ title: PropTypes.any,
+ value: PropTypes.any.isRequired
+ }),
+ PropTypes.object
+ ]),
+ placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.any]),
+ position: PropTypes.oneOf(Object.values(SelectPosition)),
+ selectedOptions: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string]))
+ ]),
+ splitButtonVariant: PropTypes.oneOf(Object.values(SplitButtonVariant)),
+ toggleIcon: PropTypes.element,
+ variant: PropTypes.oneOf([...Object.values(SelectVariant)])
};
+/**
+ * Default props.
+ *
+ * @type {{toggleIcon: null, className: string, ariaLabel: string, onSelect: Function, isToggleText: boolean, isDropdownButton: boolean,
+ * maxHeight: null, buttonVariant: string, name: null, options: *[], selectedOptions: null, variant: SelectVariant.single,
+ * isInline: boolean, id: string, isDisabled: boolean, placeholder: string, position: DropdownPosition.left, splitButtonVariant: null,
+ * direction: DropdownDirection.down}}
+ */
DropdownSelect.defaultProps = {
+ ariaLabel: 'Select option',
+ buttonVariant: ButtonVariant.default,
className: '',
+ direction: SelectDirection.down,
id: helpers.generateId(),
- multiselect: false,
+ isDisabled: false,
+ isDropdownButton: false,
+ isInline: true,
+ isToggleText: true,
+ maxHeight: null,
name: null,
onSelect: helpers.noop,
options: [],
- pullRight: false,
- selectValue: null,
- title: 'Select option'
+ placeholder: 'Select option',
+ position: SelectPosition.left,
+ selectedOptions: null,
+ splitButtonVariant: null,
+ toggleIcon: null,
+ variant: SelectVariant.single
};
-export { DropdownSelect as default, DropdownSelect };
+export {
+ DropdownSelect as default,
+ DropdownSelect,
+ ButtonVariant,
+ formatOptions,
+ formatButtonProps,
+ formatSelectProps,
+ SelectDirection,
+ SelectPosition,
+ SelectVariant,
+ SelectButtonVariant,
+ SplitButtonVariant
+};
diff --git a/src/components/form/__tests__/__snapshots__/formHelpers.test.js.snap b/src/components/form/__tests__/__snapshots__/formHelpers.test.js.snap
new file mode 100644
index 00000000..223c1311
--- /dev/null
+++ b/src/components/form/__tests__/__snapshots__/formHelpers.test.js.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FormHelpers should have specific helpers: helpers 1`] = `
+Object {
+ "createMockEvent": [Function],
+ "doesNotHaveMinimumCharacters": [Function],
+}
+`;
+
+exports[`FormHelpers should return a boolean for not having the correct number of characters: array 1`] = `true`;
+
+exports[`FormHelpers should return a boolean for not having the correct number of characters: null 1`] = `true`;
+
+exports[`FormHelpers should return a boolean for not having the correct number of characters: plain object 1`] = `true`;
+
+exports[`FormHelpers should return a boolean for not having the correct number of characters: string, 1 chars expect 1 1`] = `false`;
+
+exports[`FormHelpers should return a boolean for not having the correct number of characters: string, 2 chars expect 2 1`] = `false`;
+
+exports[`FormHelpers should return a boolean for not having the correct number of characters: string, 3 chars expect 2 1`] = `false`;
+
+exports[`FormHelpers should return a boolean for not having the correct number of characters: string, 4 chars expect 5 1`] = `true`;
+
+exports[`FormHelpers should return a boolean for not having the correct number of characters: string, 5 chars expect 5 1`] = `false`;
+
+exports[`FormHelpers should return a boolean for not having the correct number of characters: undefined 1`] = `true`;
+
+exports[`FormHelpers should return a mocked event object: mock event 1`] = `
+Object {
+ "checked": undefined,
+ "currentTarget": Object {},
+ "id": undefined,
+ "keyCode": undefined,
+ "name": undefined,
+ "persist": [Function],
+ "target": Object {},
+ "value": undefined,
+}
+`;
diff --git a/src/components/form/__tests__/__snapshots__/textInput.test.js.snap b/src/components/form/__tests__/__snapshots__/textInput.test.js.snap
new file mode 100644
index 00000000..a6ce8ce8
--- /dev/null
+++ b/src/components/form/__tests__/__snapshots__/textInput.test.js.snap
@@ -0,0 +1,135 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TextInput Component should handle readOnly, disabled: active 1`] = `
+
+`;
+
+exports[`TextInput Component should handle readOnly, disabled: disabled 1`] = `
+
+`;
+
+exports[`TextInput Component should handle readOnly, disabled: readOnly 1`] = `
+
+`;
+
+exports[`TextInput Component should render a basic component: basic component 1`] = `
+
+`;
+
+exports[`TextInput Component should return a mouseup event on text clear: emulated event, mouseup 1`] = `
+Array [
+ Array [
+ Object {
+ "checked": undefined,
+ "currentTarget": Object {
+ "value": "",
+ },
+ "id": undefined,
+ "keyCode": undefined,
+ "name": undefined,
+ "persist": [Function],
+ "target": Object {},
+ "value": "",
+ },
+ ],
+]
+`;
+
+exports[`TextInput Component should return an emulated onChange event: emulated event, change 1`] = `
+Array [
+ Array [
+ Object {
+ "checked": undefined,
+ "currentTarget": Object {
+ "value": "dolor sit",
+ },
+ "id": undefined,
+ "keyCode": undefined,
+ "name": undefined,
+ "persist": [Function],
+ "target": Object {},
+ "value": "dolor sit",
+ },
+ ],
+]
+`;
+
+exports[`TextInput Component should return an emulated onClear event on escape with type search: emulated event, esc, type search 1`] = `
+Array [
+ Array [
+ Object {
+ "checked": undefined,
+ "currentTarget": Object {
+ "value": "",
+ },
+ "id": undefined,
+ "keyCode": 27,
+ "name": undefined,
+ "persist": [Function],
+ "target": Object {},
+ "value": "",
+ },
+ ],
+]
+`;
+
+exports[`TextInput Component should return an emulated onClear event on escape: emulated event, esc 1`] = `
+Array [
+ Array [
+ Object {
+ "checked": undefined,
+ "currentTarget": Object {
+ "value": "",
+ },
+ "id": undefined,
+ "keyCode": 27,
+ "name": undefined,
+ "persist": [Function],
+ "target": Object {},
+ "value": "",
+ },
+ ],
+]
+`;
diff --git a/src/components/form/__tests__/formHelpers.test.js b/src/components/form/__tests__/formHelpers.test.js
new file mode 100644
index 00000000..2a948872
--- /dev/null
+++ b/src/components/form/__tests__/formHelpers.test.js
@@ -0,0 +1,23 @@
+import { formHelpers } from '../formHelpers';
+
+describe('FormHelpers', () => {
+ it('should have specific helpers', () => {
+ expect(formHelpers).toMatchSnapshot('helpers');
+ });
+
+ it('should return a mocked event object', () => {
+ expect(formHelpers.createMockEvent()).toMatchSnapshot('mock event');
+ });
+
+ it('should return a boolean for not having the correct number of characters', () => {
+ expect(formHelpers.doesNotHaveMinimumCharacters(null)).toMatchSnapshot('null');
+ expect(formHelpers.doesNotHaveMinimumCharacters(undefined)).toMatchSnapshot('undefined');
+ expect(formHelpers.doesNotHaveMinimumCharacters({})).toMatchSnapshot('plain object');
+ expect(formHelpers.doesNotHaveMinimumCharacters([])).toMatchSnapshot('array');
+ expect(formHelpers.doesNotHaveMinimumCharacters('l', 1)).toMatchSnapshot('string, 1 chars expect 1');
+ expect(formHelpers.doesNotHaveMinimumCharacters('lo', 2)).toMatchSnapshot('string, 2 chars expect 2');
+ expect(formHelpers.doesNotHaveMinimumCharacters('lor', 2)).toMatchSnapshot('string, 3 chars expect 2');
+ expect(formHelpers.doesNotHaveMinimumCharacters('lore', 5)).toMatchSnapshot('string, 4 chars expect 5');
+ expect(formHelpers.doesNotHaveMinimumCharacters('lorem', 5)).toMatchSnapshot('string, 5 chars expect 5');
+ });
+});
diff --git a/src/components/form/__tests__/textInput.test.js b/src/components/form/__tests__/textInput.test.js
new file mode 100644
index 00000000..43f3bfb8
--- /dev/null
+++ b/src/components/form/__tests__/textInput.test.js
@@ -0,0 +1,93 @@
+import React from 'react';
+import { TextInput as PfTextInput } from '@patternfly/react-core';
+import { TextInput } from '../textInput';
+import { helpers } from '../../../common';
+
+describe('TextInput Component', () => {
+ it('should render a basic component', async () => {
+ const props = {};
+
+ const component = await shallowHookComponent( );
+ expect(component.render()).toMatchSnapshot('basic component');
+ });
+
+ it('should handle readOnly, disabled', async () => {
+ const props = {
+ isReadOnly: true
+ };
+
+ const component = await mountHookComponent( );
+ expect(component.render()).toMatchSnapshot('readOnly');
+
+ component.setProps({
+ isReadOnly: false,
+ isDisabled: true
+ });
+
+ expect(component.render()).toMatchSnapshot('disabled');
+
+ component.setProps({
+ isReadOnly: false,
+ isDisabled: false
+ });
+
+ expect(component.render()).toMatchSnapshot('active');
+ });
+
+ it('should return an emulated onChange event', async () => {
+ const mockOnChange = jest.fn();
+ const props = {
+ onChange: mockOnChange,
+ value: 'lorem ipsum'
+ };
+
+ const component = await shallowHookComponent( );
+ const mockEvent = { currentTarget: { value: 'dolor sit' }, persist: helpers.noop };
+ component.find(PfTextInput).simulate('change', 'hello world', mockEvent);
+
+ expect(mockOnChange.mock.calls).toMatchSnapshot('emulated event, change');
+ });
+
+ it('should return an emulated onClear event on escape', async () => {
+ const mockOnClear = jest.fn();
+ const props = {
+ onClear: mockOnClear,
+ value: 'lorem ipsum'
+ };
+
+ const component = await shallowHookComponent( );
+ const mockEvent = { keyCode: 27, currentTarget: { value: '' }, persist: helpers.noop };
+ component.find(PfTextInput).simulate('keyup', mockEvent);
+
+ expect(mockOnClear.mock.calls).toMatchSnapshot('emulated event, esc');
+ });
+
+ it('should return an emulated onClear event on escape with type search', async () => {
+ const mockOnClear = jest.fn();
+ const props = {
+ onClear: mockOnClear,
+ value: 'lorem ipsum',
+ type: 'search'
+ };
+
+ const component = await shallowHookComponent( );
+ const mockEvent = { keyCode: 27, currentTarget: { value: '' }, persist: helpers.noop };
+ component.find(PfTextInput).simulate('keyup', mockEvent);
+
+ expect(mockOnClear.mock.calls).toMatchSnapshot('emulated event, esc, type search');
+ });
+
+ it('should return a mouseup event on text clear', async () => {
+ const mockOnMouseUp = jest.fn();
+ const props = {
+ onMouseUp: mockOnMouseUp,
+ value: 'lorem ipsum'
+ };
+
+ const component = await shallowHookComponent( );
+ const mockEvent = { currentTarget: { value: '' }, persist: helpers.noop };
+ component.find(PfTextInput).simulate('mouseup', mockEvent);
+
+ expect(mockOnMouseUp.mock.calls).toMatchSnapshot('emulated event, mouseup');
+ });
+});
diff --git a/src/components/form/formHelpers.js b/src/components/form/formHelpers.js
new file mode 100644
index 00000000..0ed98dbe
--- /dev/null
+++ b/src/components/form/formHelpers.js
@@ -0,0 +1,43 @@
+import { helpers } from '../../common';
+
+/**
+ * Create a consistent mock event object.
+ *
+ * @param {object} event
+ * @param {boolean} persistEvent
+ * @returns {{keyCode, currentTarget, name, id: *, persist: Function, value, target}}
+ */
+const createMockEvent = (event, persistEvent = false) => {
+ const { checked, currentTarget = {}, keyCode, persist = helpers.noop, target = {} } = { ...event };
+ if (persistEvent) {
+ persist();
+ }
+
+ return {
+ checked,
+ currentTarget,
+ keyCode,
+ id: currentTarget.id || currentTarget.name,
+ name: currentTarget.name,
+ persist,
+ value: currentTarget.value,
+ target
+ };
+};
+
+/**
+ * Confirm a string has minimum length.
+ *
+ * @param {string} value
+ * @param {number} characters
+ * @returns {boolean}
+ */
+const doesNotHaveMinimumCharacters = (value, characters = 1) =>
+ (typeof value === 'string' && value.length < characters) || typeof value !== 'string';
+
+const formHelpers = {
+ createMockEvent,
+ doesNotHaveMinimumCharacters
+};
+
+export { formHelpers as default, formHelpers, createMockEvent, doesNotHaveMinimumCharacters };
diff --git a/src/components/form/textInput.js b/src/components/form/textInput.js
new file mode 100644
index 00000000..7b2c3bad
--- /dev/null
+++ b/src/components/form/textInput.js
@@ -0,0 +1,163 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { TextInput as PfTextInput } from '@patternfly/react-core';
+import { createMockEvent } from './formHelpers';
+import { helpers } from '../../common';
+
+/**
+ * A wrapper for Patternfly TextInput.
+ * Provides a consistent event structure, and an onClear event for the search type.
+ *
+ * @fires onKeyUp
+ * @fires onMouseUp
+ * @fires onChange
+ * @param {object} props
+ * @param {*|string} props.value
+ * @param {string} props.className
+ * @param {string} props.id
+ * @param {boolean} props.isDisabled
+ * @param {string} props.name
+ * @param {Function} props.onChange
+ * @param {Function} props.onClear
+ * @param {Function} props.onKeyUp
+ * @param {Function} props.onMouseUp
+ * @param {boolean} props.isReadOnly
+ * @param {string} props.type
+ * @returns {React.ReactNode};
+ */
+const TextInput = ({
+ className,
+ id,
+ isDisabled,
+ name,
+ onChange,
+ onClear,
+ onKeyUp,
+ onMouseUp,
+ isReadOnly,
+ type,
+ value,
+ ...props
+}) => {
+ const [updatedValue, setUpdatedValue] = useState(value);
+
+ /**
+ * onKeyUp event, provide additional functionality for onClear event.
+ *
+ * @event onKeyUp
+ * @param {object} event
+ */
+ const onTextInputKeyUp = event => {
+ const { currentTarget, keyCode } = event;
+ const clonedEvent = { ...event };
+
+ onKeyUp(createMockEvent(event, true));
+
+ if (keyCode === 27) {
+ if (type === 'search' && currentTarget.value === '') {
+ onClear(createMockEvent(clonedEvent));
+ } else {
+ setUpdatedValue('');
+ onClear(createMockEvent({ ...clonedEvent, ...{ currentTarget: { ...clonedEvent.currentTarget, value: '' } } }));
+ }
+ }
+ };
+
+ /**
+ * onMouseUp event, provide additional functionality for onClear event.
+ *
+ * @event onMouseUp
+ * @param {object} event
+ */
+ const onTextInputMouseUp = event => {
+ const { currentTarget } = event;
+ const clonedEvent = { ...event };
+
+ onMouseUp(createMockEvent(event, true));
+
+ if (type !== 'search' || currentTarget.value === '') {
+ return;
+ }
+
+ window.setTimeout(() => {
+ if (currentTarget.value === '') {
+ onClear(createMockEvent(clonedEvent));
+ }
+ });
+ };
+
+ /**
+ * onChange event, provide restructured event.
+ *
+ * @event onChange
+ * @param {string} changedValue
+ * @param {object} event
+ */
+ const onTextInputChange = (changedValue, event) => {
+ const clonedEvent = { ...event };
+
+ setUpdatedValue(changedValue);
+ onChange(createMockEvent(clonedEvent));
+ };
+
+ const updatedName = name || helpers.generateId();
+ const updatedId = id || updatedName;
+
+ return (
+
+ );
+};
+
+/**
+ * Prop types
+ *
+ * @type {{onKeyUp: Function, isReadOnly: boolean, onChange: Function, onClear: Function, name: string,
+ * className: string, id: string, isDisabled: boolean, onMouseUp: Function, type: string, value: string}}
+ */
+TextInput.propTypes = {
+ className: PropTypes.string,
+ id: PropTypes.string,
+ isDisabled: PropTypes.bool,
+ isReadOnly: PropTypes.bool,
+ name: PropTypes.string,
+ onChange: PropTypes.func,
+ onClear: PropTypes.func,
+ onKeyUp: PropTypes.func,
+ onMouseUp: PropTypes.func,
+ type: PropTypes.string,
+ value: PropTypes.string
+};
+
+/**
+ * Default props
+ *
+ * @type {{onKeyUp: Function, isReadOnly: boolean, onChange: Function, onClear: Function, name: null,
+ * className: string, id: null, isDisabled: boolean, onMouseUp: Function, type: string, value: string}}
+ */
+TextInput.defaultProps = {
+ className: '',
+ id: null,
+ isDisabled: false,
+ isReadOnly: false,
+ name: null,
+ onChange: helpers.noop,
+ onClear: helpers.noop,
+ onKeyUp: helpers.noop,
+ onMouseUp: helpers.noop,
+ type: 'text',
+ value: ''
+};
+
+export { TextInput as default, TextInput };
diff --git a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap
index 23f37db8..4249d282 100644
--- a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap
+++ b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap
@@ -14,25 +14,664 @@ Array [
Object {
"file": "./src/components/aboutModal/aboutModal.js",
"keys": Array [
+ Object {
+ "key": "about-modal.copyright",
+ "match": "t('about-modal.copyright', { year: currentYear })",
+ },
+ Object {
+ "key": "about-modal.copy-confirmation",
+ "match": "t('about-modal.copy-confirmation')",
+ },
Object {
"key": "about-modal.username",
- "match": "t('about-modal.username', 'Username')",
+ "match": "t('about-modal.username')",
},
Object {
"key": "about-modal.browser-version",
- "match": "t('about-modal.browser-version', 'Browser Version')",
+ "match": "t('about-modal.browser-version')",
},
Object {
"key": "about-modal.browser-os",
- "match": "t('about-modal.browser-os', 'Browser OS')",
+ "match": "t('about-modal.browser-os')",
},
Object {
"key": "about-modal.server-version",
- "match": "t('about-modal.server-version', 'Server Version')",
+ "match": "t('about-modal.server-version')",
},
Object {
"key": "about-modal.ui-version",
- "match": "t('about-modal.ui-version', 'UI Version')",
+ "match": "t('about-modal.ui-version')",
+ },
+ Object {
+ "key": "about-modal.copy-button",
+ "match": "t('about-modal.copy-button')",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/addCredentialType/addCredentialType.js",
+ "keys": Array [
+ Object {
+ "key": "form-dialog.label",
+ "match": "translate('form-dialog.label', { context: ['option', 'network'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "translate('form-dialog.label', { context: ['option', 'satellite'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "translate('form-dialog.label', { context: ['option', 'vcenter'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: ['placeholder', 'add-credential'] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/addSourceWizard/addSourceWizard.js",
+ "keys": Array [
+ Object {
+ "key": "form-dialog.confirmation_title_add-source",
+ "match": "t('form-dialog.confirmation_title_add-source', { context: [pending && 'exit', edit && 'edit'] })",
+ },
+ Object {
+ "key": "form-dialog.confirmation_heading_add-source",
+ "match": "t('form-dialog.confirmation_heading_add-source', { context: [pending && 'exit', edit && 'edit'] })",
+ },
+ Object {
+ "key": "form-dialog.confirmation_body_add-source",
+ "match": "t('form-dialog.confirmation_body_add-source', { context: [pending && 'exit', edit && 'edit', !pending && EMPTY_CONTEXT] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'no' })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'yes' })",
+ },
+ Object {
+ "key": "form-dialog.title",
+ "match": "t('form-dialog.title', { context: ['add-source', edit && 'edit'] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/addSourceWizard/addSourceWizardConstants.js",
+ "keys": Array [
+ Object {
+ "key": "form-dialog.title",
+ "match": "translate('form-dialog.title', { context: ['add-source', 'step'] })",
+ },
+ Object {
+ "key": "form-dialog.title",
+ "match": "translate('form-dialog.title', { context: ['add-source', 'step', 'two'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "translate('form-dialog.label', { context: ['submit', 'add-source'] })",
+ },
+ Object {
+ "key": "form-dialog.title",
+ "match": "translate('form-dialog.title', { context: ['add-source', 'step', 'three'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "translate('form-dialog.label', { context: ['close'] })",
+ },
+ Object {
+ "key": "form-dialog.title",
+ "match": "translate('form-dialog.title', { context: ['add-source', 'step', 'two'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "translate('form-dialog.label', { context: ['submit', 'add-source'] })",
+ },
+ Object {
+ "key": "form-dialog.title",
+ "match": "translate('form-dialog.title', { context: ['add-source', 'step', 'three'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "translate('form-dialog.label', { context: ['close'] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/addSourceWizard/addSourceWizardContext.js",
+ "keys": Array [
+ Object {
+ "key": "toast-notifications.title_add-source_hidden",
+ "match": "t('toast-notifications.title_add-source_hidden', { context: [error && 'error', edit && 'edit'] })",
+ },
+ Object {
+ "key": "toast-notifications.description_add-source_hidden",
+ "match": "t('toast-notifications.description_add-source_hidden', { context: [error && 'error', edit && 'edit'], message: errorMessage, name: source?.[apiTypes.API_SUBMIT_SOURCE_NAME] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/addSourceWizard/addSourceWizardStepThree.js",
+ "keys": Array [
+ Object {
+ "key": "form-dialog.empty-state_title_add-source",
+ "match": "t('form-dialog.empty-state_title_add-source', { context: [(error && 'error')",
+ },
+ Object {
+ "key": "form-dialog.empty-state_description_add-source",
+ "match": "t(\`form-dialog.empty-state_description_add-source\`, { context: [(error && 'error')",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/addSourceWizard/addSourceWizardStepTwo.js",
+ "keys": Array [
+ Object {
+ "key": "form-dialog.label",
+ "match": "translate('form-dialog.label', { context: ['option', type] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "translate('form-dialog.label', { context: ['option', 'disableSsl'] })",
+ },
+ Object {
+ "key": "form-dialog.label_placeholder",
+ "match": "t('form-dialog.label_placeholder', { context: [ 'add-source', 'credential', multiselectCredentials && 'multi', !availableCredentials.length && 'add' ] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'add-credential' })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'add-credential' })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/authentication/authentication.js",
+ "keys": Array [
+ Object {
+ "key": "view.loading",
+ "match": "t('view.loading', { context: 'authentication' })",
+ },
+ Object {
+ "key": "view.error",
+ "match": "t('view.error', { context: 'authentication' })",
+ },
+ Object {
+ "key": "view.error-message",
+ "match": "t( 'view.error-message', { context: 'authentication', message: \`\${session.errorMessage.replace(/\\\\.$/, '')",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/confirmationModal/confirmationModal.js",
+ "keys": Array [
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: ['submit', 'confirmation'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'cancel' })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: ['submit', 'confirmation'] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/createCredentialDialog/createCredentialDialog.js",
+ "keys": Array [
+ Object {
+ "key": "form-dialog.label",
+ "match": "translate('form-dialog.label', { context: ['option', type] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: ['submit', 'create-credential'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'cancel' })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/createScanDialog/createScanDialog.js",
+ "keys": Array [
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: ['submit', 'create-scan'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'cancel' })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/credentials/credentials.js",
+ "keys": Array [
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'add' })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'delete' })",
+ },
+ Object {
+ "key": "view.loading",
+ "match": "t('view.loading', { context: 'credentials' })",
+ },
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['filter', 'title'] })",
+ },
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['filter', 'description'] })",
+ },
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['label', 'clear'] })",
+ },
+ Object {
+ "key": "view.error",
+ "match": "t('view.error', { context: 'credentials' })",
+ },
+ Object {
+ "key": "view.error-message",
+ "match": "t('view.error-message', { context: ['credentials'], message: errorMessage })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/credentials/credentialsEmptyState.js",
+ "keys": Array [
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: 'title', name: uiShortName })",
+ },
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['description', 'credentials'], name: uiSentenceStartName })",
+ },
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['label', 'source'] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/mergeReportsDialog/mergeReportsDialog.js",
+ "keys": Array [
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'close' })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: ['submit', 'merge-reports'] })",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: 'cancel' })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/modal/modal.js",
+ "keys": Array [
+ Object {
+ "key": "modal.aria-label-default",
+ "match": "t('modal.aria-label-default')",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/refreshTimeButton/refreshTimeButton.js",
+ "keys": Array [
+ Object {
+ "key": "refresh-time-button.refreshed",
+ "match": "t('refresh-time-button.refreshed', { context: lastRefresh && 'load', refresh: lastRefresh && helpers.getTimeDisplayHowLongAgo(lastRefresh)",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/scanHostList/scanHostList.js",
+ "keys": Array [
+ Object {
+ "key": "view.loading",
+ "match": "t('view.loading')",
+ },
+ Object {
+ "key": "view.error",
+ "match": "t('view.error', { context: 'scan-hosts' })",
+ },
+ Object {
+ "key": "view.error-message",
+ "match": "t('view.error-message', { context: ['scan-hosts'], message: errorMessage })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/scans/scanJobsList.js",
+ "keys": Array [
+ Object {
+ "key": "view.loading",
+ "match": "t('view.loading')",
+ },
+ Object {
+ "key": "view.error",
+ "match": "t('view.error', { context: 'scan-jobs' })",
+ },
+ Object {
+ "key": "view.error-message",
+ "match": "t('view.error-message', { context: ['scan-jobs'], message: errorMessage })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/scans/scans.js",
+ "keys": Array [
+ Object {
+ "key": "table.tooltip",
+ "match": "t('table.tooltip', { context: ['merge-reports'] })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: ['merge-reports'] })",
+ },
+ Object {
+ "key": "view.loading",
+ "match": "t('view.loading', { context: viewId })",
+ },
+ Object {
+ "key": "view.error",
+ "match": "t('view.error', { context: viewId })",
+ },
+ Object {
+ "key": "view.error-message",
+ "match": "t('view.error-message', { context: [viewId], message: errorMessage })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['description'] })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['scan'] })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['success', viewId] })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['failed', viewId] })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['sources'] })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['scan-jobs'] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/scans/scansContext.js",
+ "keys": Array [
+ Object {
+ "key": "toast-notifications.description",
+ "match": "t( 'toast-notifications.description', { context: ['scan-report', scanContext], name: scanName || scanId }, [ ] )",
+ },
+ Object {
+ "key": "toast-notifications.title",
+ "match": "t('toast-notifications.title', { context: [(isWarning && 'warning')",
+ },
+ Object {
+ "key": "toast-notifications.description",
+ "match": "t('toast-notifications.description', { context: [(isWarning && 'warning')",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/scans/scansEmptyState.js",
+ "keys": Array [
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['title', viewId], count: sourcesCount, name: uiShortName })",
+ },
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['description', viewId], count: sourcesCount })",
+ },
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['label', 'source-navigate'], count: sourcesCount })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/scans/scanSourceList.js",
+ "keys": Array [
+ Object {
+ "key": "view.loading",
+ "match": "t('view.loading')",
+ },
+ Object {
+ "key": "view.error",
+ "match": "t('view.error', { context: 'scan-jobs' })",
+ },
+ Object {
+ "key": "view.error-message",
+ "match": "t('view.error-message', { context: ['scan-jobs'], message: errorMessage })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/scans/scansTableCells.js",
+ "keys": Array [
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: ['status', status, 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: ['action', 'scan', context] })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: ['action', 'scan', context] })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: ['action', 'scan', context] })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: ['action', 'scan', 'download'] })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: ['action', 'scan', 'download'] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/sources/sources.js",
+ "keys": Array [
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: 'add' })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: 'scan' })",
+ },
+ Object {
+ "key": "view.loading",
+ "match": "t('view.loading', { context: viewId })",
+ },
+ Object {
+ "key": "view.error",
+ "match": "t('view.error', { context: viewId })",
+ },
+ Object {
+ "key": "view.error-message",
+ "match": "t('view.error-message', { context: [viewId], message: errorMessage })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['description'] })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['scan'] })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['credentials'] })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['success', viewId] })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['failed', viewId] })",
+ },
+ Object {
+ "key": "table.header",
+ "match": "t('table.header', { context: ['unreachable', viewId] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/sources/sourcesContext.js",
+ "keys": Array [
+ Object {
+ "key": "toast-notifications.title",
+ "match": "t('toast-notifications.title', { context: ['deleted-source'] })",
+ },
+ Object {
+ "key": "toast-notifications.description",
+ "match": "t('toast-notifications.description', { context: ['deleted-source'], name: sourceName })",
+ },
+ Object {
+ "key": "toast-notifications.title",
+ "match": "t('toast-notifications.title', { context: ['error'] })",
+ },
+ Object {
+ "key": "form-dialog.confirmation",
+ "match": "t('form-dialog.confirmation', { context: ['title', 'delete-source'] })",
+ },
+ Object {
+ "key": "form-dialog.confirmation",
+ "match": "t( 'form-dialog.confirmation', { context: ['heading', 'delete-source'], name: source[apiTypes.API_RESPONSE_SOURCE_NAME] }, [ ] )",
+ },
+ Object {
+ "key": "form-dialog.label",
+ "match": "t('form-dialog.label', { context: ['delete'] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/sources/sourcesEmptyState.js",
+ "keys": Array [
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['title'], name: uiShortName })",
+ },
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['description', viewId] })",
+ },
+ Object {
+ "key": "view.empty-state",
+ "match": "t('view.empty-state', { context: ['label', viewId] })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/sources/sourcesTableCells.js",
+ "keys": Array [
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: 'network-range' })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: ['status', status, 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: 'edit' })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: 'edit' })",
+ },
+ 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: 'scan' })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: 'edit' })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: 'delete' })",
+ },
+ Object {
+ "key": "table.label",
+ "match": "t('table.label', { context: 'Scan' })",
+ },
+ ],
+ },
+ Object {
+ "file": "./src/components/table/tableEmpty.js",
+ "keys": Array [
+ Object {
+ "key": "table.empty-state_title",
+ "match": "t('table.empty-state_title', 'No results found')",
+ },
+ Object {
+ "key": "table.empty-state_description",
+ "match": "t('table.empty-state_description', 'Clear all filters and try again.')",
},
],
},
@@ -42,24 +681,204 @@ Array [
exports[`I18n Component should have locale keys that exist in the default language JSON: predictable missing locale keys 1`] = `
Array [
Object {
- "file": "./src/components/aboutModal/aboutModal.js",
- "key": "about-modal.username",
+ "file": "./src/components/addSourceWizard/addSourceWizard.js",
+ "key": "form-dialog.title",
},
Object {
- "file": "./src/components/aboutModal/aboutModal.js",
- "key": "about-modal.browser-version",
+ "file": "./src/components/addSourceWizard/addSourceWizardConstants.js",
+ "key": "form-dialog.title",
},
Object {
- "file": "./src/components/aboutModal/aboutModal.js",
- "key": "about-modal.browser-os",
+ "file": "./src/components/addSourceWizard/addSourceWizardConstants.js",
+ "key": "form-dialog.title",
},
Object {
- "file": "./src/components/aboutModal/aboutModal.js",
- "key": "about-modal.server-version",
+ "file": "./src/components/addSourceWizard/addSourceWizardConstants.js",
+ "key": "form-dialog.title",
},
Object {
- "file": "./src/components/aboutModal/aboutModal.js",
- "key": "about-modal.ui-version",
+ "file": "./src/components/addSourceWizard/addSourceWizardConstants.js",
+ "key": "form-dialog.title",
+ },
+ Object {
+ "file": "./src/components/addSourceWizard/addSourceWizardConstants.js",
+ "key": "form-dialog.title",
+ },
+ Object {
+ "file": "./src/components/addSourceWizard/addSourceWizardStepTwo.js",
+ "key": "form-dialog.label_placeholder",
+ },
+ Object {
+ "file": "./src/components/authentication/authentication.js",
+ "key": "view.error",
+ },
+ Object {
+ "file": "./src/components/authentication/authentication.js",
+ "key": "view.error-message",
+ },
+ Object {
+ "file": "./src/components/credentials/credentials.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/credentials/credentials.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/credentials/credentials.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/credentials/credentials.js",
+ "key": "view.error",
+ },
+ Object {
+ "file": "./src/components/credentials/credentials.js",
+ "key": "view.error-message",
+ },
+ Object {
+ "file": "./src/components/credentials/credentialsEmptyState.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/credentials/credentialsEmptyState.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/credentials/credentialsEmptyState.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/scanHostList/scanHostList.js",
+ "key": "view.error",
+ },
+ Object {
+ "file": "./src/components/scanHostList/scanHostList.js",
+ "key": "view.error-message",
+ },
+ Object {
+ "file": "./src/components/scans/scanJobsList.js",
+ "key": "view.error",
+ },
+ Object {
+ "file": "./src/components/scans/scanJobsList.js",
+ "key": "view.error-message",
+ },
+ Object {
+ "file": "./src/components/scans/scans.js",
+ "key": "table.tooltip",
+ },
+ Object {
+ "file": "./src/components/scans/scans.js",
+ "key": "view.error",
+ },
+ Object {
+ "file": "./src/components/scans/scans.js",
+ "key": "view.error-message",
+ },
+ Object {
+ "file": "./src/components/scans/scans.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/scans/scans.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/scans/scans.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/scans/scans.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/scans/scans.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/scans/scans.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/scans/scansEmptyState.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/scans/scansEmptyState.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/scans/scansEmptyState.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/scans/scanSourceList.js",
+ "key": "view.error",
+ },
+ Object {
+ "file": "./src/components/scans/scanSourceList.js",
+ "key": "view.error-message",
+ },
+ Object {
+ "file": "./src/components/sources/sources.js",
+ "key": "view.error",
+ },
+ Object {
+ "file": "./src/components/sources/sources.js",
+ "key": "view.error-message",
+ },
+ Object {
+ "file": "./src/components/sources/sources.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/sources/sources.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/sources/sources.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/sources/sources.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/sources/sources.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/sources/sources.js",
+ "key": "table.header",
+ },
+ Object {
+ "file": "./src/components/sources/sourcesContext.js",
+ "key": "form-dialog.confirmation",
+ },
+ Object {
+ "file": "./src/components/sources/sourcesContext.js",
+ "key": "form-dialog.confirmation",
+ },
+ Object {
+ "file": "./src/components/sources/sourcesEmptyState.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/sources/sourcesEmptyState.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/sources/sourcesEmptyState.js",
+ "key": "view.empty-state",
+ },
+ Object {
+ "file": "./src/components/table/tableEmpty.js",
+ "key": "table.empty-state_title",
+ },
+ Object {
+ "file": "./src/components/table/tableEmpty.js",
+ "key": "table.empty-state_description",
},
]
`;
diff --git a/src/components/i18n/__tests__/__snapshots__/i18nHelpers.test.js.snap b/src/components/i18n/__tests__/__snapshots__/i18nHelpers.test.js.snap
index 8c463297..f87c42c3 100644
--- a/src/components/i18n/__tests__/__snapshots__/i18nHelpers.test.js.snap
+++ b/src/components/i18n/__tests__/__snapshots__/i18nHelpers.test.js.snap
@@ -3,10 +3,10 @@
exports[`I18nHelpers should attempt to perform a string replace: translate 1`] = `
Object {
"emptyContext": "t(lorem.ipsum, {\\"context\\":\\" \\"})",
- "emptyPartialContext": "t(lorem.ipsum, {\\"context\\":\\"hello_ \\"})",
+ "emptyPartialContext": "t(lorem.ipsum_hello, {\\"context\\":\\" \\"})",
"localeKey": "t(lorem.ipsum)",
- "multiContext": "t(lorem.ipsum, {\\"context\\":\\"hello_world\\"})",
- "multiContextWithEmptyValue": "t(lorem.ipsum, {\\"context\\":\\"hello_world\\"})",
+ "multiContext": "t(lorem.ipsum_hello, {\\"context\\":\\"world\\"})",
+ "multiContextWithEmptyValue": "t(lorem.ipsum_hello, {\\"context\\":\\"world\\"})",
"multiKey": "t([lorem.ipsum,lorem.fallback])",
"placeholder": "t(lorem.ipsum, hello world)",
}
diff --git a/src/components/i18n/i18nHelpers.js b/src/components/i18n/i18nHelpers.js
index a163447e..22211433 100644
--- a/src/components/i18n/i18nHelpers.js
+++ b/src/components/i18n/i18nHelpers.js
@@ -22,7 +22,7 @@ const EMPTY_CONTEXT = 'LOCALE_EMPTY_CONTEXT';
* @returns {string|React.ReactNode}
*/
const translate = (translateKey, values = null, components, { emptyContextValue = EMPTY_CONTEXT } = {}) => {
- const updatedValues = values;
+ const updatedValues = values || {};
let updatedTranslateKey = translateKey;
if (Array.isArray(updatedTranslateKey)) {
@@ -30,10 +30,23 @@ const translate = (translateKey, values = null, components, { emptyContextValue
}
if (Array.isArray(updatedValues?.context)) {
- updatedValues.context = updatedValues.context
+ const updatedContext = updatedValues.context
.map(value => (value === emptyContextValue && ' ') || value)
- .filter(value => typeof value === 'string' && value.length > 0)
- .join('_');
+ .filter(value => typeof value === 'string' && value.length > 0);
+
+ if (updatedContext?.length > 1) {
+ const lastContext = updatedContext.pop();
+
+ if (Array.isArray(updatedTranslateKey)) {
+ updatedTranslateKey[0] = `${updatedTranslateKey[0]}_${updatedContext.join('_')}`;
+ } else {
+ updatedTranslateKey = `${updatedTranslateKey}_${updatedContext.join('_')}`;
+ }
+
+ updatedValues.context = lastContext;
+ } else {
+ updatedValues.context = updatedContext.join('_');
+ }
} else if (updatedValues?.context === emptyContextValue) {
updatedValues.context = ' ';
}
diff --git a/src/components/listStatusItem/__tests__/__snapshots__/listStatusItem.test.js.snap b/src/components/listStatusItem/__tests__/__snapshots__/listStatusItem.test.js.snap
deleted file mode 100644
index 63b08de6..00000000
--- a/src/components/listStatusItem/__tests__/__snapshots__/listStatusItem.test.js.snap
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ListStatusItem Component should render 1`] = `
-
-`;
diff --git a/src/components/listStatusItem/__tests__/listStatusItem.test.js b/src/components/listStatusItem/__tests__/listStatusItem.test.js
deleted file mode 100644
index d9615df6..00000000
--- a/src/components/listStatusItem/__tests__/listStatusItem.test.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import ListStatusItem from '../listStatusItem';
-
-describe('ListStatusItem Component', () => {
- it('should render', () => {
- const props = {
- key: 'credential',
- id: 'credential',
- count: 100,
- emptyText: '0 Credentials',
- tipSingular: 'Credential',
- tipPlural: 'Credentials',
- expanded: true,
- expandType: 'credentials',
- toggleExpand: jest.fn(),
- iconInfo: { type: 'fa', name: 'id-card' }
- };
-
- const component = mount( );
-
- expect(component.render()).toMatchSnapshot();
- });
-});
diff --git a/src/components/listStatusItem/listStatusItem.js b/src/components/listStatusItem/listStatusItem.js
deleted file mode 100644
index 178c9972..00000000
--- a/src/components/listStatusItem/listStatusItem.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import cx from 'classnames';
-import { Icon, ListView } from 'patternfly-react';
-import _get from 'lodash/get';
-import Tooltip from '../tooltip/tooltip';
-import helpers from '../../common/helpers';
-
-const ListStatusItem = ({ count, emptyText, tipSingular, tipPlural, expanded, expandType, toggleExpand, iconInfo }) => {
- if (count <= 0) {
- return (
-
-
- {emptyText}
-
-
- );
- }
-
- return (
-
-
- {
- toggleExpand(expandType);
- }}
- >
- {iconInfo && (
-
-
- {count}
-
- )}
- {!iconInfo && (
-
- {count}
- {` ${tipPlural}`}
-
- )}
-
-
-
- );
-};
-
-ListStatusItem.propTypes = {
- count: PropTypes.number,
- emptyText: PropTypes.string,
- tipSingular: PropTypes.string,
- tipPlural: PropTypes.string,
- expanded: PropTypes.bool,
- expandType: PropTypes.string,
- toggleExpand: PropTypes.func,
- iconInfo: PropTypes.shape({
- classNames: PropTypes.array,
- name: PropTypes.string,
- type: PropTypes.string
- })
-};
-
-ListStatusItem.defaultProps = {
- count: 0,
- emptyText: null,
- tipSingular: null,
- tipPlural: null,
- expanded: false,
- expandType: null,
- toggleExpand: helpers.noop,
- iconInfo: null
-};
-
-export { ListStatusItem as default, ListStatusItem };
diff --git a/src/components/mergeReportsDialog/__tests__/__snapshots__/mergeReportsDialog.test.js.snap b/src/components/mergeReportsDialog/__tests__/__snapshots__/mergeReportsDialog.test.js.snap
index a3f01921..f3853431 100644
--- a/src/components/mergeReportsDialog/__tests__/__snapshots__/mergeReportsDialog.test.js.snap
+++ b/src/components/mergeReportsDialog/__tests__/__snapshots__/mergeReportsDialog.test.js.snap
@@ -2,48 +2,41 @@
exports[`MergeReportsDialog Component should render a component, pending: pending 1`] = `
-
-
-
-
+ Merge reports
+
+
+
+
@@ -53,24 +46,32 @@ exports[`MergeReportsDialog Component should render a component, pending: pendin
Merging reports...
-
+
@@ -80,48 +81,41 @@ exports[`MergeReportsDialog Component should render a connected component: conne
exports[`MergeReportsDialog Component should render a non-connected component, failure and success: non-connected 1`] = `
-
-
-
-
+ Merge reports
+
+
+
+
@@ -164,24 +158,32 @@ exports[`MergeReportsDialog Component should render a non-connected component, f
-
+
diff --git a/src/components/mergeReportsDialog/mergeReportsDialog.js b/src/components/mergeReportsDialog/mergeReportsDialog.js
index 4d7a038d..33f7bc75 100644
--- a/src/components/mergeReportsDialog/mergeReportsDialog.js
+++ b/src/components/mergeReportsDialog/mergeReportsDialog.js
@@ -1,9 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Modal, Button, Icon, Spinner } from 'patternfly-react';
+import { Button, ButtonVariant, Title } from '@patternfly/react-core';
+import { Icon, Spinner } from 'patternfly-react';
+import { Modal } from '../modal/modal';
import { connect, reduxActions, reduxTypes, store } from '../../redux';
import { helpers } from '../../common/helpers';
import { apiTypes } from '../../constants/apiConstants';
+import { translate } from '../i18n/i18n';
class MergeReportsDialog extends React.Component {
onClose = () => {
@@ -34,7 +37,7 @@ class MergeReportsDialog extends React.Component {
error => {
store.dispatch({
type: reduxTypes.toastNotifications.TOAST_ADD,
- alertType: 'error',
+ alertType: 'danger',
header: 'Error merging reports',
message: helpers.getMessageFromResults(error).message
});
@@ -97,26 +100,25 @@ class MergeReportsDialog extends React.Component {
}
renderButtons() {
+ const { t } = this.props;
const validCount = this.getValidScans().length;
if (validCount === 0) {
- return (
-
- Close
+ return [
+
+ {t('form-dialog.label', { context: 'close' })}
- );
+ ];
}
- return (
-
-
- Cancel
-
-
- Merge
-
-
- );
+ return [
+
+ {t('form-dialog.label', { context: ['submit', 'merge-reports'] })}
+ ,
+
+ {t('form-dialog.label', { context: 'cancel' })}
+
+ ];
}
render() {
@@ -154,35 +156,31 @@ class MergeReportsDialog extends React.Component {
}
return (
-
-
-
-
-
- Merge reports
-
-
- {pending && (
-
-
- Merging reports...
-
- )}
- {!pending && (
-
-
{icon}
-
- {heading}
-
- {this.renderValidScans()}
- {this.renderInvalidScans()}
- {footer}
-
-
-
- )}
-
- {this.renderButtons()}
+ Merge reports}
+ actions={this.renderButtons()}
+ >
+ {pending && (
+
+
+ Merging reports...
+
+ )}
+ {!pending && (
+
+
{icon}
+
+ {heading}
+
+ {this.renderValidScans()}
+ {this.renderInvalidScans()}
+ {footer}
+
+
+
+ )}
);
}
@@ -200,14 +198,16 @@ MergeReportsDialog.propTypes = {
name: PropTypes.string
})
),
- show: PropTypes.bool.isRequired
+ show: PropTypes.bool.isRequired,
+ t: PropTypes.func
};
MergeReportsDialog.defaultProps = {
getReportsDownload: helpers.noop,
mergeReports: helpers.noop,
pending: false,
- scans: []
+ scans: [],
+ t: translate
};
const mapDispatchToProps = dispatch => ({
diff --git a/src/components/modal/__tests__/__snapshots__/modal.test.js.snap b/src/components/modal/__tests__/__snapshots__/modal.test.js.snap
new file mode 100644
index 00000000..f4f41df8
--- /dev/null
+++ b/src/components/modal/__tests__/__snapshots__/modal.test.js.snap
@@ -0,0 +1,440 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Modal Component should allow custom headers and footers: element 1`] = `
+
+`;
+
+exports[`Modal Component should allow custom headers and footers: function 1`] = `
+
+`;
+
+exports[`Modal Component should allow custom headers and footers: list 1`] = `
+
+`;
+
+exports[`Modal Component should allow custom headers and footers: string 1`] = `
+
+`;
+
+exports[`Modal Component should allow custom headers and footers: undefined 1`] = `
+
+`;
+
+exports[`Modal Component should allow modifying specific and custom props: aria-label 1`] = `
+
+
+
+ }
+ aria-describedby=""
+ aria-label="dolor sit"
+ aria-labelledby=""
+ className=""
+ hasNoBodyWrapper={false}
+ isOpen={false}
+ onClose={[Function]}
+ ouiaSafe={true}
+ position="top"
+ positionOffset="5%"
+ showClose={false}
+ title=""
+ titleIconVariant={null}
+ titleLabel=""
+ variant={null}
+>
+
}
+ >
+
+
+
+`;
+
+exports[`Modal Component should allow modifying specific and custom props: backdrop 1`] = `
+
+
+
+ }
+ aria-describedby=""
+ aria-label="t(modal.aria-label-default)"
+ aria-labelledby=""
+ className=""
+ hasNoBodyWrapper={false}
+ isOpen={false}
+ onClose={[Function]}
+ ouiaSafe={true}
+ position="top"
+ positionOffset="5%"
+ showClose={false}
+ title=""
+ titleIconVariant={null}
+ titleLabel=""
+ variant={null}
+>
+
}
+ >
+
+
+
+`;
+
+exports[`Modal Component should allow modifying specific and custom props: isContentOnly 1`] = `
+
+
+
+ }
+ aria-describedby=""
+ aria-label="t(modal.aria-label-default)"
+ aria-labelledby=""
+ className=""
+ hasNoBodyWrapper={true}
+ isOpen={false}
+ onClose={[Function]}
+ ouiaSafe={true}
+ position="top"
+ positionOffset="5%"
+ showClose={false}
+ title=""
+ titleIconVariant={null}
+ titleLabel=""
+ variant={null}
+>
+ }
+ >
+
+
+
+`;
+
+exports[`Modal Component should render a basic component: basic 1`] = `
+
+
+
+
+ }
+ aria-describedby=""
+ aria-label="t(modal.aria-label-default)"
+ aria-labelledby=""
+ className=""
+ hasNoBodyWrapper={false}
+ isOpen={false}
+ onClose={[Function]}
+ ouiaSafe={true}
+ position="top"
+ positionOffset="5%"
+ showClose={false}
+ title=""
+ titleIconVariant={null}
+ titleLabel=""
+ variant={null}
+ >
+ }
+ >
+
+
+
+
+`;
diff --git a/src/components/modal/__tests__/modal.test.js b/src/components/modal/__tests__/modal.test.js
new file mode 100644
index 00000000..2e685866
--- /dev/null
+++ b/src/components/modal/__tests__/modal.test.js
@@ -0,0 +1,79 @@
+import React from 'react';
+import { Modal as PfModal, ModalContent } from '@patternfly/react-core';
+import { Modal } from '../modal';
+
+describe('Modal Component', () => {
+ it('should render a basic component', async () => {
+ const props = {};
+
+ const component = await mountHookComponent(lorem ipsum );
+ expect(component).toMatchSnapshot('basic');
+ });
+
+ it('should allow modifying specific and custom props', async () => {
+ const props = {
+ backdrop: false
+ };
+
+ const backdropComponent = await mountHookComponent(lorem ipsum );
+ expect(backdropComponent.find(PfModal)).toMatchSnapshot('backdrop');
+
+ props.backdrop = true;
+ props['aria-label'] = 'dolor sit';
+
+ const ariaLabelComponent = await mountHookComponent(lorem ipsum );
+ expect(ariaLabelComponent.find(PfModal)).toMatchSnapshot('aria-label');
+
+ props.backdrop = false;
+ props['aria-label'] = undefined;
+ props.isContentOnly = true;
+
+ const contentComponent = await mountHookComponent(lorem ipsum );
+ expect(contentComponent.find(PfModal)).toMatchSnapshot('isContentOnly');
+ });
+
+ it('should allow custom headers and footers', async () => {
+ // disableFocusTrap for testing only
+ const props = {
+ isOpen: true,
+ disableFocusTrap: true
+ };
+
+ const component = await mountHookComponent(hello world );
+
+ component.setProps({
+ header: undefined,
+ footer: undefined
+ });
+
+ expect(component.find(ModalContent).render()).toMatchSnapshot('undefined');
+
+ component.setProps({
+ header: 'lorem ipsum',
+ footer: 'dolor sit'
+ });
+
+ expect(component.find(ModalContent).render()).toMatchSnapshot('string');
+
+ component.setProps({
+ header: () => 'lorem ipsum',
+ footer: () => 'dolor sit'
+ });
+
+ expect(component.find(ModalContent).render()).toMatchSnapshot('function');
+
+ component.setProps({
+ header: [lorem ipsum ],
+ footer: [dolor sit ]
+ });
+
+ expect(component.find(ModalContent).render()).toMatchSnapshot('list');
+
+ component.setProps({
+ header: lorem ipsum ,
+ footer: dolor sit
+ });
+
+ expect(component.find(ModalContent).render()).toMatchSnapshot('element');
+ });
+});
diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js
new file mode 100644
index 00000000..6139c7ea
--- /dev/null
+++ b/src/components/modal/modal.js
@@ -0,0 +1,134 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { useMount, useUnmount } from 'react-use';
+import { Modal as PfModal, ModalProps, ModalVariant } from '@patternfly/react-core';
+import classNames from 'classnames';
+import { translate } from '../i18n/i18n';
+
+/**
+ * Wrapper for adjusting PF Modal styling.
+ *
+ * @param {object} props
+ * @param {string} props.'aria-label'
+ * @param {boolean} props.backdrop
+ * @param {React.ReactNode} props.children
+ * @param {string} props.className
+ * @param {React.ReactNode|Function} props.header
+ * @param {React.ReactNode|Function} props.footer
+ * @param {boolean} props.isContentOnly
+ * @param {string} props.position
+ * @param {string} props.positionOffset
+ * @param {boolean} props.showClose
+ * @param {Function} props.t
+ * @param {string} props.variant
+ * @param {ModalProps} props.props
+ * @returns {React.ReactNode}
+ */
+const Modal = ({
+ 'aria-label': ariaLabel,
+ backdrop,
+ children,
+ className,
+ header,
+ footer,
+ isContentOnly,
+ position,
+ positionOffset,
+ showClose,
+ t,
+ variant,
+ ...props
+}) => {
+ const [element, setElement] = useState();
+ const updatedProps = { ...props };
+ const cssClassName = classNames(
+ `quipucords-modal`,
+ { 'quipucords-modal__hide-backdrop': backdrop === false },
+ { 'quipucords-modal__rcue-width': !variant },
+ className
+ );
+
+ useMount(() => {
+ const domElement = document.createElement('div');
+ document.body.appendChild(domElement);
+ setElement(domElement);
+ });
+
+ useUnmount(() => {
+ element?.remove();
+ });
+
+ if (!element) {
+ return null;
+ }
+
+ element.className = cssClassName;
+
+ if (header) {
+ updatedProps.header = (typeof header === 'function' && header()) || header;
+ }
+
+ if (footer) {
+ updatedProps.footer = (typeof footer === 'function' && footer()) || footer;
+ }
+
+ return (
+
+ {(React.isValidElement(children) && children) || {children || ''}
}
+
+ );
+};
+
+/**
+ * Prop types
+ *
+ * @type {{backdrop: boolean, showClose: boolean, t: Function, children: React.ReactNode,
+ * footer: React.ReactNode|Function, variant: string, header: React.ReactNode|Function,
+ * className: string|object, isContentOnly: boolean, position: string, positionOffset: string,
+ * 'aria-label': string}}
+ */
+Modal.propTypes = {
+ 'aria-label': PropTypes.string,
+ backdrop: PropTypes.bool,
+ children: PropTypes.node.isRequired,
+ className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ footer: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
+ header: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
+ isContentOnly: PropTypes.bool,
+ position: PropTypes.oneOf(['top', null]),
+ positionOffset: PropTypes.string,
+ showClose: PropTypes.bool,
+ t: PropTypes.func,
+ variant: PropTypes.oneOf([...Object.values(ModalVariant)])
+};
+
+/**
+ * Default props
+ *
+ * @type {{backdrop: boolean, showClose: boolean, t: translate, footer: null, variant: null, header: null,
+ * className: null, isContentOnly: boolean, position: string, positionOffset: string, 'aria-label': null}}
+ */
+Modal.defaultProps = {
+ 'aria-label': null,
+ backdrop: true,
+ className: null,
+ footer: null,
+ header: null,
+ isContentOnly: false,
+ position: 'top',
+ positionOffset: '5%',
+ showClose: false,
+ t: translate,
+ variant: null
+};
+
+export { Modal as default, Modal, ModalVariant };
diff --git a/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap b/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap
index 8585724d..0c3f060c 100644
--- a/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap
+++ b/src/components/pageLayout/__tests__/__snapshots__/pageLayout.test.js.snap
@@ -363,24 +363,14 @@ exports[`PageLayout Component should render a non-connected component unauthoriz
menu={
Array [
Object {
- "component": Object {
- "$$typeof": Symbol(react.memo),
- "WrappedComponent": [Function],
- "compare": null,
- "type": [Function],
- },
+ "component": [Function],
"iconClass": "fa fa-crosshairs",
"redirect": true,
"title": "Sources",
"to": "/sources",
},
Object {
- "component": Object {
- "$$typeof": Symbol(react.memo),
- "WrappedComponent": [Function],
- "compare": null,
- "type": [Function],
- },
+ "component": [Function],
"iconClass": "pficon pficon-orders",
"title": "Scans",
"to": "/scans",
diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js
index 89c8d8cf..13ed4647 100644
--- a/src/components/poll/poll.js
+++ b/src/components/poll/poll.js
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { helpers } from '../../common';
const PollCache = {
timer: null,
@@ -44,7 +45,7 @@ class Poll extends React.Component {
}
Poll.propTypes = {
- children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
+ children: PropTypes.node.isRequired,
itemId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
itemIdCheck: PropTypes.bool.isRequired,
interval: PropTypes.number,
@@ -52,7 +53,7 @@ Poll.propTypes = {
};
Poll.defaultProps = {
- interval: 60000
+ interval: helpers.POLL_INTERVAL
};
export { Poll as default, Poll, PollCache };
diff --git a/src/components/refreshTimeButton/__tests__/__snapshots__/refreshTimeButton.test.js.snap b/src/components/refreshTimeButton/__tests__/__snapshots__/refreshTimeButton.test.js.snap
index e7cd742e..ca67d420 100644
--- a/src/components/refreshTimeButton/__tests__/__snapshots__/refreshTimeButton.test.js.snap
+++ b/src/components/refreshTimeButton/__tests__/__snapshots__/refreshTimeButton.test.js.snap
@@ -2,17 +2,34 @@
exports[`RefreshTimeButton Component should render 1`] = `
+ class="pf-c-button__icon pf-m-start"
+ >
+
+
+
+
- Refreshed 0
+ t(refresh-time-button.refreshed, {"context":0,"refresh":0})
`;
diff --git a/src/components/refreshTimeButton/refreshTimeButton.js b/src/components/refreshTimeButton/refreshTimeButton.js
index 451f13de..5dc1b5de 100644
--- a/src/components/refreshTimeButton/refreshTimeButton.js
+++ b/src/components/refreshTimeButton/refreshTimeButton.js
@@ -1,7 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Button, Icon } from 'patternfly-react';
+import { Button } from '@patternfly/react-core';
+import { RebootingIcon } from '@patternfly/react-icons';
import { helpers } from '../../common/helpers';
+import { translate } from '../i18n/i18n';
class RefreshTimeButton extends React.Component {
pollingInterval = null;
@@ -43,13 +45,15 @@ class RefreshTimeButton extends React.Component {
}
render() {
- const { lastRefresh, onRefresh } = this.props;
+ const { lastRefresh, onRefresh, t } = this.props;
return (
-
-
+ } onClick={onRefresh}>
- Refreshed {lastRefresh && helpers.getTimeDisplayHowLongAgo(lastRefresh)}
+ {t('refresh-time-button.refreshed', {
+ context: lastRefresh && 'load',
+ refresh: lastRefresh && helpers.getTimeDisplayHowLongAgo(lastRefresh)
+ })}
);
@@ -58,11 +62,13 @@ class RefreshTimeButton extends React.Component {
RefreshTimeButton.propTypes = {
lastRefresh: PropTypes.number,
- onRefresh: PropTypes.func.isRequired
+ onRefresh: PropTypes.func.isRequired,
+ t: PropTypes.func
};
RefreshTimeButton.defaultProps = {
- lastRefresh: 0
+ lastRefresh: 0,
+ t: translate
};
export { RefreshTimeButton as default, RefreshTimeButton };
diff --git a/src/components/router/__tests__/__snapshots__/router.test.js.snap b/src/components/router/__tests__/__snapshots__/router.test.js.snap
index cb7fc7cb..131c11b0 100644
--- a/src/components/router/__tests__/__snapshots__/router.test.js.snap
+++ b/src/components/router/__tests__/__snapshots__/router.test.js.snap
@@ -6,26 +6,12 @@ exports[`Router Component should shallow render a basic component 1`] = `
>
diff --git a/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap b/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap
index 6c50690f..06913230 100644
--- a/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap
+++ b/src/components/router/__tests__/__snapshots__/routerConstants.test.js.snap
@@ -5,24 +5,14 @@ exports[`RouterTypes should return specific properties: baseName 1`] = `"/client
exports[`RouterTypes should return specific properties: routes 1`] = `
Array [
Object {
- "component": Object {
- "$$typeof": Symbol(react.memo),
- "WrappedComponent": [Function],
- "compare": null,
- "type": [Function],
- },
+ "component": [Function],
"iconClass": "fa fa-crosshairs",
"redirect": true,
"title": "Sources",
"to": "/sources",
},
Object {
- "component": Object {
- "$$typeof": Symbol(react.memo),
- "WrappedComponent": [Function],
- "compare": null,
- "type": [Function],
- },
+ "component": [Function],
"iconClass": "pficon pficon-orders",
"title": "Scans",
"to": "/scans",
diff --git a/src/components/router/routerConstants.js b/src/components/router/routerConstants.js
index 3c7d7b3a..e843776c 100644
--- a/src/components/router/routerConstants.js
+++ b/src/components/router/routerConstants.js
@@ -1,16 +1,18 @@
import Scans from '../scans/scans';
-import Sources from '../sources/sources';
+import { Sources } from '../sources/sources';
import Credentials from '../credentials/credentials';
/**
* Return the application base directory.
+ *
* @type {string}
*/
const baseName = '/client';
/**
* Return array of objects that describe navigation
- * @return {array}
+ *
+ * @returns {Array}
*/
const routes = [
{
diff --git a/src/components/scanHostList/__tests__/__snapshots__/scanHostList.test.js.snap b/src/components/scanHostList/__tests__/__snapshots__/scanHostList.test.js.snap
index e4fd85f5..eacd02af 100644
--- a/src/components/scanHostList/__tests__/__snapshots__/scanHostList.test.js.snap
+++ b/src/components/scanHostList/__tests__/__snapshots__/scanHostList.test.js.snap
@@ -17,53 +17,46 @@ exports[`ScanHostList Component should render a connected component: connected 1
exports[`ScanHostList Component should render a non-connected component error: error 1`] = `
-
-
- Error retrieving scan results
-
-
- Lorem Ipsum.
-
+ t(view.error-message, {"context":"scan-hosts","message":"Lorem Ipsum."})
+
`;
exports[`ScanHostList Component should render a non-connected component pending: pending 1`] = `
-
- Loading...
-
+
+ t(view.loading)
`;
exports[`ScanHostList Component should render a non-connected component: non-connected 1`] = `
-
-
+ {"credentialName":"dolor","jobType":"connection","name":"lorem","sourceId":15,"sourceName":"lorem source"}
+
+
- {"credentialName":"dolor","jobType":"connection","name":"lorem","sourceId":15,"sourceName":"lorem source"}{"credentialName":"set","jobType":"inspection","name":"ipsum","sourceId":16,"sourceName":"ipsum source"}
-
-
+ {"credentialName":"set","jobType":"inspection","name":"ipsum","sourceId":16,"sourceName":"ipsum source"}
+
+
`;
diff --git a/src/components/scanHostList/scanHostList.js b/src/components/scanHostList/scanHostList.js
index 203546ad..07640707 100644
--- a/src/components/scanHostList/scanHostList.js
+++ b/src/components/scanHostList/scanHostList.js
@@ -1,10 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { EmptyState, Grid, Spinner } from 'patternfly-react';
+import { Alert, AlertVariant, EmptyState, EmptyStateVariant, List, ListItem, Spinner } from '@patternfly/react-core';
import { connect, reduxActions, reduxSelectors } from '../../redux';
import { helpers } from '../../common/helpers';
import { apiTypes } from '../../constants/apiConstants';
+import { translate } from '../i18n/i18n';
+/**
+ * Return a scan hosts listing for "hosts".
+ */
class ScanHostList extends React.Component {
state = {
currentPage: 1,
@@ -65,37 +69,43 @@ class ScanHostList extends React.Component {
};
render() {
- const { children, error, errorMessage, hostsList, pending } = this.props;
+ const { children, error, errorMessage, hostsList, pending, t } = this.props;
- if (error) {
+ if (pending) {
return (
-
-
- Error retrieving scan results
- {errorMessage}
+
+ {t('view.loading')}
);
}
- if (pending) {
+ if (error) {
return (
-
-
- Loading...
+
+
+ {t('view.error-message', { context: ['scan-hosts'], message: errorMessage })}
+
);
}
return (
-
-
- {hostsList.map(host => children({ host }))}
-
-
+
+ {hostsList?.map(host => (
+ {children({ host })}
+ ))}
+
);
}
}
+/**
+ * Default props
+ *
+ * @type {{useInspectionResults: boolean, pending: boolean, errorMessage: string, getInspectionScanResults: Function,
+ * error: boolean, hostsList: Array, filter: object, t: Function, children: React.ReactNode, isMoreResults: boolean,
+ * id: string|number, getConnectionScanResults: Function, useConnectionResults: boolean}}
+ */
ScanHostList.propTypes = {
children: PropTypes.func.isRequired,
error: PropTypes.bool,
@@ -116,10 +126,18 @@ ScanHostList.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
isMoreResults: PropTypes.bool,
pending: PropTypes.bool,
+ t: PropTypes.func,
useConnectionResults: PropTypes.bool,
useInspectionResults: PropTypes.bool
};
+/**
+ * Default props
+ *
+ * @type {{filter: {}, useInspectionResults: boolean, t: translate, isMoreResults: boolean, pending: boolean,
+ * errorMessage: null, getInspectionScanResults: Function, error: boolean, getConnectionScanResults: Function,
+ * useConnectionResults: boolean, hostsList: *[]}}
+ */
ScanHostList.defaultProps = {
error: false,
errorMessage: null,
@@ -129,6 +147,7 @@ ScanHostList.defaultProps = {
hostsList: [],
isMoreResults: false,
pending: false,
+ t: translate,
useConnectionResults: false,
useInspectionResults: false
};
diff --git a/src/components/scans/__tests__/__snapshots__/scanDownload.test.js.snap b/src/components/scans/__tests__/__snapshots__/scanDownload.test.js.snap
index 35316f7c..0ec5a495 100644
--- a/src/components/scans/__tests__/__snapshots__/scanDownload.test.js.snap
+++ b/src/components/scans/__tests__/__snapshots__/scanDownload.test.js.snap
@@ -19,66 +19,135 @@ exports[`ScanDownload Component should have an optional tooltip: tooltip 1`] = `
tooltip="Lorem ipsum dolor sit"
>
-
- Lorem ipsum dolor sit
-
- }
- placement="top"
- rootClose={true}
- trigger={
- Array [
- "hover",
- ]
- }
+
-
+
+
+ Lorem ipsum dolor sit
+
+
+ }
+ 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={
+
+
+ Download
+
+
+ }
+ zIndex={9999}
>
-
-
- Download
-
-
-
-
+
+
+ Download
+
+
+
+
+
+
`;
diff --git a/src/components/scans/__tests__/__snapshots__/scanJobsList.test.js.snap b/src/components/scans/__tests__/__snapshots__/scanJobsList.test.js.snap
index a7c8fbb7..c197c2f8 100644
--- a/src/components/scans/__tests__/__snapshots__/scanJobsList.test.js.snap
+++ b/src/components/scans/__tests__/__snapshots__/scanJobsList.test.js.snap
@@ -8,42 +8,30 @@ exports[`ScanJobsList Component should render a connected component: connected 1
exports[`ScanJobsList Component should render a non-connected component error: error 1`] = `
-
-
- Error retrieving scan jobs
-
-
- Lorem Ipsum.
-
+ t(view.error-message, {"context":"scan-jobs","message":"Lorem Ipsum."})
+
`;
exports[`ScanJobsList Component should render a non-connected component pending: pending 1`] = `
-
- Loading...
-
+
+ t(view.loading)
`;
@@ -70,11 +58,11 @@ exports[`ScanJobsList Component should render a non-connected component: non-con
sm={3}
xs={6}
>
-
+
Completed
-
+
10
-
+
10
-
 Download
diff --git a/src/components/scans/__tests__/__snapshots__/scanListItem.test.js.snap b/src/components/scans/__tests__/__snapshots__/scanListItem.test.js.snap
deleted file mode 100644
index 3efe73f7..00000000
--- a/src/components/scans/__tests__/__snapshots__/scanListItem.test.js.snap
+++ /dev/null
@@ -1,183 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ScanListItem Component should render a connected component: connected 1`] = `
-
-`;
-
-exports[`ScanListItem Component should render a non-connected component: non-connected 1`] = `
-
-
-
-
-
-
-
-
-
- }
- additionalInfo={
- Array [
- ,
- ,
- ,
- ,
- ]
- }
- checkboxInput={
-
- }
- className="quipucords-scan-list-item list-view-pf-top-align"
- compoundExpand={true}
- compoundExpanded={false}
- description={
-
-
-
-
- Lorem ipsum
-
-
-
-
-
-
- }
- heading={null}
- hideCloseIcon={false}
- initExpanded={false}
- key="42"
- leftContent={
-
- lorem
-
- }
- onCloseCompoundExpand={[Function]}
- onExpand={[Function]}
- onExpandClose={[Function]}
- stacked={false}
- />
-
-`;
diff --git a/src/components/scans/__tests__/__snapshots__/scanSourceList.test.js.snap b/src/components/scans/__tests__/__snapshots__/scanSourceList.test.js.snap
index 7439430e..29dc41d1 100644
--- a/src/components/scans/__tests__/__snapshots__/scanSourceList.test.js.snap
+++ b/src/components/scans/__tests__/__snapshots__/scanSourceList.test.js.snap
@@ -1,114 +1,96 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScanSourceList Component should handle multiple status messages: connect status 1`] = `
-
-
-
-
- Â
- test
-
-
- Connection Scan: ipsum
-
-
-
+
+ }
+ key="test"
+ >
+ test
+
+
+ Connection Scan: ipsum
+
+
+
+
`;
exports[`ScanSourceList Component should handle multiple status messages: fallback status 1`] = `
-
-
-
-
- Â
- test
-
-
- Connection Scan: ipsum
-
-
-
+
+ }
+ key="test"
+ >
+ test
+
+
+ Connection Scan: ipsum
+
+
+
+
`;
exports[`ScanSourceList Component should handle multiple status messages: inspect status 1`] = `
-
-
-
-
- Â
- test
-
-
- Inspection Scan: sit
-
-
-
+
+ }
+ key="test"
+ >
+ test
+
+
+ Inspection Scan: sit
+
+
+
+
`;
exports[`ScanSourceList Component should render a connected component: connected 1`] = `
@@ -119,66 +101,71 @@ exports[`ScanSourceList Component should render a connected component: connected
exports[`ScanSourceList Component should render a non-connected component error: error 1`] = `
-
-
- Error retrieving scan jobs
-
-
- Lorem Ipsum.
-
+ t(view.error-message, {"context":"scan-jobs","message":"Lorem Ipsum."})
+
`;
exports[`ScanSourceList Component should render a non-connected component pending: pending 1`] = `
-
- Loading...
-
+
+ t(view.loading)
`;
exports[`ScanSourceList Component should render a non-connected component: non-connected 1`] = `
-
-
-
-
- Â test
-
-
- Connection Scan: ipsum
-
-
-
+
+
+
+
+
+
+ test
+
+
+ Connection Scan: ipsum
+
+
+
+
`;
diff --git a/src/components/scans/__tests__/__snapshots__/scans.test.js.snap b/src/components/scans/__tests__/__snapshots__/scans.test.js.snap
index 2572f8a3..062bd0ef 100644
--- a/src/components/scans/__tests__/__snapshots__/scans.test.js.snap
+++ b/src/components/scans/__tests__/__snapshots__/scans.test.js.snap
@@ -1,111 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Scans Component should render a connected component with default props: connected 1`] = ` `;
-
-exports[`Scans Component should render a non-connected component error: error 1`] = `
+exports[`Scans Component should handle multiple display states, pending, error, fulfilled: error 1`] = `
-
- Error retrieving scans:
-
+ t(view.error-message, {"context":"scans"})
`;
-exports[`Scans Component should render a non-connected component pending: pending 1`] = `
-
-
-
-
-
- Loading...
-
-
-
-
-`;
-
-exports[`Scans Component should render a non-connected component with empty state: empty state 1`] = ` `;
-
-exports[`Scans Component should render a non-connected component: non-connected 1`] = `
+exports[`Scans Component should handle multiple display states, pending, error, fulfilled: fulfilled 1`] = `
-
-
- Merge reports
-
-
-
+ t(table.label, {"context":"merge-reports"})
+
+
}
activeFilters={Array []}
filterFields={
@@ -128,10 +56,9 @@ exports[`Scans Component should render a non-connected component: non-connected
filterValue=""
itemsType="Scan"
itemsTypePlural="Scans"
- lastRefresh={0}
+ lastRefresh={NaN}
onRefresh={[Function]}
selectedCount={0}
- selectedItems={Array []}
sortAscending={true}
sortFields={
Array [
@@ -155,27 +82,358 @@ exports[`Scans Component should render a non-connected component: non-connected
-
-
+
+
+
+
+ lorem
+
+
+
+ ,
+ "dataLabel": "t(table.header, {\\"context\\":\\"description\\"})",
+ },
+ Object {
+ "content":
+
+
+
+
+
+ t(table.label_status, {"context":"scans"})
+
+ a day ago
+
+ ,
+ "dataLabel": "t(table.header, {\\"context\\":\\"scan\\"})",
+ "width": 20,
+ },
+ Object {
+ "content":
+ t(table.label_status_cell, {"context":"scans","count":0}, [object Object],[object Object])
+ ,
+ "dataLabel": "t(table.header_success, {\\"context\\":\\"scans\\"})",
+ "expandedContent": undefined,
+ "isExpanded": false,
+ "width": 8,
+ },
+ Object {
+ "content":
+ t(table.label_status_cell, {"context":"scans","count":0}, [object Object],[object Object])
+ ,
+ "dataLabel": "t(table.header_failed, {\\"context\\":\\"scans\\"})",
+ "expandedContent": undefined,
+ "isExpanded": false,
+ "width": 8,
+ },
+ Object {
+ "content":
+ t(table.label_status_cell, {"context":"scans","count":0}, [object Object],[object Object])
+ ,
+ "dataLabel": "t(table.header, {\\"context\\":\\"sources\\"})",
+ "expandedContent": undefined,
+ "isExpanded": false,
+ "width": 8,
+ },
+ Object {
+ "content":
+ t(table.label_status_cell, {"context":"scans","count":0}, [object Object],[object Object])
+ ,
+ "dataLabel": "t(table.header, {\\"context\\":\\"scan-jobs\\"})",
+ "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[`Scans Component should handle multiple display states, pending, error, fulfilled: pending 1`] = `
+
+
+
+ t(view.loading, {"context":"scans"})
+
+
+`;
+
+exports[`Scans Component should render a basic component: basic 1`] = `
+
+`;
+
+exports[`Scans Component should return an empty state when there are no scans: empty state 1`] = `
+
+
+
+
+
+
+
+
+
+
+ t(view.empty-state_title, {"context":"scans","count":0,"name":"Quipucords"})
+
+
+ t(view.empty-state_description, {"context":"scans","count":0})
+
+
+
+ t(view.empty-state_label, {"context":"source-navigate","count":0})
+
+
+
+
+
+
`;
diff --git a/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap b/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap
new file mode 100644
index 00000000..e4d48761
--- /dev/null
+++ b/src/components/scans/__tests__/__snapshots__/scansContext.test.js.snap
@@ -0,0 +1,104 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ScansContext should apply a hook for retreiving 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[`ScansContext should attempt to poll scans: timeout 1`] = `
+Array [
+ Array [
+ Object {
+ "callback": false,
+ "interval": 0,
+ },
+ ],
+ Array [
+ Object {
+ "callback": false,
+ "interval": 0,
+ },
+ ],
+]
+`;
+
+exports[`ScansContext should handle scan actions with multiple callbacks: callbacks 1`] = `
+Object {
+ "onCancel": [Function],
+ "onDownload": [Function],
+ "onPause": [Function],
+ "onRestart": [Function],
+ "onStart": [Function],
+}
+`;
+
+exports[`ScansContext should handle scan actions with multiple callbacks: dispatch onStart 1`] = `
+Array [
+ Array [
+ Object {
+ "meta": Object {
+ "id": "lorem ipsum base id",
+ },
+ "payload": Promise {},
+ "type": "START_SCAN",
+ },
+ ],
+]
+`;
+
+exports[`ScansContext should return specific properties: specific properties 1`] = `
+Object {
+ "useGetScans": [Function],
+ "useOnExpand": [Function],
+ "useOnRefresh": [Function],
+ "useOnScanAction": [Function],
+ "useOnSelect": [Function],
+ "usePoll": [Function],
+}
+`;
diff --git a/src/components/scans/__tests__/__snapshots__/scansEmptyState.test.js.snap b/src/components/scans/__tests__/__snapshots__/scansEmptyState.test.js.snap
index 9d7e8976..7b49064a 100644
--- a/src/components/scans/__tests__/__snapshots__/scansEmptyState.test.js.snap
+++ b/src/components/scans/__tests__/__snapshots__/scansEmptyState.test.js.snap
@@ -8,46 +8,51 @@ exports[`ScansEmptyState Component should render a connected component: connecte
exports[`ScansEmptyState Component should render a non-connected component: non-connected do not exist 1`] = `
+
+
+
+
+ t(view.empty-state, {"context":"title","count":0,"name":"Quipucords"})
+
-
-
-
-
- Welcome to Quipucords
-
-
- Begin by adding one or more sources. A source contains a collection of network information,
-
- including systems management solution information, IP addresses, or host names, in addition to
-
- SSH ports and credentials.
-
-
+
+
-
- Add Source
-
-
+ t(view.empty-state_label, {"context":"source-navigate","count":0})
+
@@ -55,42 +60,51 @@ exports[`ScansEmptyState Component should render a non-connected component: non-
exports[`ScansEmptyState Component should render a non-connected component: non-connected exist 1`] = `
+
+
+
+
+ t(view.empty-state, {"context":"title","count":0,"name":"Quipucords"})
+
-
-
-
-
- No scans exist yet
-
-
- Select a Source to scan from the Sources page.
-
-
+
+
-
- Go to Sources
-
-
+ t(view.empty-state_label, {"context":"source-navigate","count":0})
+
diff --git a/src/components/scans/__tests__/__snapshots__/scansTableCells.test.js.snap b/src/components/scans/__tests__/__snapshots__/scansTableCells.test.js.snap
new file mode 100644
index 00000000..6640d7bb
--- /dev/null
+++ b/src/components/scans/__tests__/__snapshots__/scansTableCells.test.js.snap
@@ -0,0 +1,182 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ScansTableCells should export specific function components: function components 1`] = `
+Object {
+ "actionsCell": [Function],
+ "description": [Function],
+ "failedHostsCellContent": [Function],
+ "okHostsCellContent": [Function],
+ "scanStatus": [Function],
+ "scansCellContent": [Function],
+ "sourcesCellContent": [Function],
+ "statusCell": [Function],
+ "statusContent": [Function],
+}
+`;
+
+exports[`ScansTableCells should return consistent cell results: basic actionsCell cell 1`] = `
+
+
+
+
+
+
+ }
+ position="right"
+ selectedOptions={null}
+ splitButtonVariant={null}
+ toggleIcon={null}
+ variant="single"
+ />
+
+
+`;
+
+exports[`ScansTableCells should return consistent cell results: basic description cell 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`ScansTableCells should return consistent cell results: basic failedHostsCellContent cell 1`] = `
+Object {
+ "content":
+ t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object])
+ ,
+ "expandedContent": undefined,
+}
+`;
+
+exports[`ScansTableCells should return consistent cell results: basic okHostsCellContent cell 1`] = `
+Object {
+ "content":
+ t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object])
+ ,
+ "expandedContent": undefined,
+}
+`;
+
+exports[`ScansTableCells should return consistent cell results: basic scanStatus cell 1`] = `
+
+
+
+
+
+
+ t(table.label, {"context":"status"})
+
+ a day ago
+
+
+`;
+
+exports[`ScansTableCells should return consistent cell results: basic scansCellContent cell 1`] = `
+Object {
+ "content":
+ t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object])
+ ,
+ "expandedContent": undefined,
+}
+`;
+
+exports[`ScansTableCells 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[`ScansTableCells should return consistent cell results: basic statusCell cell 1`] = `
+
+ t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object])
+
+`;
+
+exports[`ScansTableCells should return consistent cell results: basic statusContent cell 1`] = `
+
+ [Function]
+
+`;
diff --git a/src/components/scans/__tests__/scanListItem.test.js b/src/components/scans/__tests__/scanListItem.test.js
deleted file mode 100644
index 342dcf3b..00000000
--- a/src/components/scans/__tests__/scanListItem.test.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import React from 'react';
-import configureMockStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
-import { shallow } from 'enzyme';
-import { ConnectedScanListItem, ScanListItem } from '../scanListItem';
-import { viewTypes } from '../../../redux/constants';
-
-describe('ScanListItem Component', () => {
- const generateEmptyStore = (obj = {}) => configureMockStore()(obj);
-
- it('should render a connected component', () => {
- const store = generateEmptyStore({ viewOptions: { [viewTypes.SCANS_VIEW]: {} } });
-
- const props = {
- lastRefresh: 0,
- scan: {
- jobsTotal: 1,
- id: 42,
- mostRecentEndTime: '',
- mostRecentId: 2,
- mostRecentReportId: 3,
- mostRecentStatus: 'completed',
- mostRecentStartTime: '',
- mostRecentStatusMessage: 'Lorem ipsum',
- mostRecentSysFailed: 1,
- mostRecentSysScanned: 20,
- name: 'lorem',
- sourcesTotal: 1
- }
- };
-
- const component = shallow(
-
-
-
- );
-
- expect(component.find(ConnectedScanListItem)).toMatchSnapshot('connected');
- });
-
- it('should render a non-connected component', () => {
- const props = {
- lastRefresh: 0,
- scan: {
- jobsTotal: 1,
- id: 42,
- mostRecentEndTime: '',
- mostRecentId: 2,
- mostRecentReportId: 3,
- mostRecentStatus: 'completed',
- mostRecentStartTime: '',
- mostRecentStatusMessage: 'Lorem ipsum',
- mostRecentSysFailed: 1,
- mostRecentSysScanned: 20,
- name: 'lorem',
- sourcesTotal: 1
- }
- };
-
- const component = shallow(
);
- expect(component).toMatchSnapshot('non-connected');
- });
-});
diff --git a/src/components/scans/__tests__/scans.test.js b/src/components/scans/__tests__/scans.test.js
index b500ef6b..cacb60b3 100644
--- a/src/components/scans/__tests__/scans.test.js
+++ b/src/components/scans/__tests__/scans.test.js
@@ -1,69 +1,64 @@
import React from 'react';
-import configureMockStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
-import { shallow } from 'enzyme';
-import { ConnectedScans, Scans } from '../scans';
+import { Scans } from '../scans';
import { apiTypes } from '../../../constants/apiConstants';
describe('Scans Component', () => {
- const generateEmptyStore = (obj = {}) => configureMockStore()(obj);
-
- it('should render a connected component with default props', () => {
- const store = generateEmptyStore({ scans: { view: {} }, viewOptions: {} });
- const component = shallow(
-
-
-
- );
-
- expect(component.find(ConnectedScans)).toMatchSnapshot('connected');
- });
-
- it('should render a non-connected component', () => {
+ it('should render a basic component', async () => {
const props = {
- fulfilled: true,
- scans: [
- {
- [apiTypes.API_RESPONSE_SCAN_ID]: 1
- }
- ],
- viewOptions: {
- selectedItems: []
- }
+ useGetScans: () => ({
+ fulfilled: true
+ })
};
- const component = shallow(
);
-
- expect(component).toMatchSnapshot('non-connected');
+ const component = await shallowHookComponent(
);
+ expect(component).toMatchSnapshot('basic');
});
- it('should render a non-connected component error', () => {
+ it('should handle multiple display states, pending, error, fulfilled', async () => {
const props = {
- error: true
+ useGetScans: () => ({
+ pending: true
+ })
};
- const component = shallow(
);
+ const component = await shallowHookComponent(
);
+ expect(component).toMatchSnapshot('pending');
- expect(component).toMatchSnapshot('error');
- });
+ component.setProps({
+ useGetScans: () => ({
+ pending: false,
+ error: true
+ })
+ });
- it('should render a non-connected component pending', () => {
- const props = {
- pending: true
- };
+ expect(component).toMatchSnapshot('error');
- const component = shallow(
);
+ component.setProps({
+ useGetScans: () => ({
+ pending: false,
+ error: false,
+ fulfilled: true,
+ data: [
+ {
+ [apiTypes.API_RESPONSE_SCAN_ID]: '1',
+ [apiTypes.API_RESPONSE_SCAN_NAME]: 'lorem'
+ }
+ ]
+ })
+ });
- expect(component).toMatchSnapshot('pending');
+ expect(component).toMatchSnapshot('fulfilled');
});
- it('should render a non-connected component with empty state', () => {
+ it('should return an empty state when there are no scans', async () => {
const props = {
- scans: []
+ useGetScans: () => ({
+ fulfilled: true,
+ data: []
+ })
};
- const component = shallow(
);
-
- expect(component).toMatchSnapshot('empty state');
+ const component = await shallowHookComponent(
);
+ expect(component.render()).toMatchSnapshot('empty state');
});
});
diff --git a/src/components/scans/__tests__/scansContext.test.js b/src/components/scans/__tests__/scansContext.test.js
new file mode 100644
index 00000000..39b56fcc
--- /dev/null
+++ b/src/components/scans/__tests__/scansContext.test.js
@@ -0,0 +1,99 @@
+import { context, useGetScans, useOnScanAction, usePoll } from '../scansContext';
+import { apiTypes } from '../../../constants/apiConstants';
+import { reduxTypes } from '../../../redux';
+
+describe('ScansContext', () => {
+ it('should return specific properties', () => {
+ expect(context).toMatchSnapshot('specific properties');
+ });
+
+ it('should handle scan actions with multiple callbacks', async () => {
+ const mockDispatch = jest.fn();
+ const mockScan = {
+ [apiTypes.API_RESPONSE_SCAN_ID]: 'lorem ipsum base id',
+ [apiTypes.API_RESPONSE_SCAN_NAME]: 'lorem ipsum name',
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: {
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_ID]: 'dolor sit id'
+ }
+ };
+
+ const { result: confirmCallbacks } = await shallowHook(() =>
+ useOnScanAction({
+ useDispatch: () => mockDispatch
+ })
+ );
+
+ confirmCallbacks.onStart(mockScan);
+
+ expect(confirmCallbacks).toMatchSnapshot('callbacks');
+ expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch onStart');
+ mockDispatch.mockClear();
+ });
+
+ it('should attempt to poll scans', async () => {
+ const mockUseTimeout = jest.fn();
+ const options = {
+ pollInterval: 0,
+ useSelector: () => [{ connection: { status: 'pending' } }],
+ useTimeout: (callback, interval) => {
+ mockUseTimeout({
+ callback: callback(),
+ interval
+ });
+ return {};
+ }
+ };
+
+ await shallowHook(() => usePoll(options));
+ await shallowHook(() =>
+ usePoll({
+ ...options,
+ useSelector: () => []
+ })
+ );
+
+ expect(mockUseTimeout.mock.calls).toMatchSnapshot('timeout');
+ });
+
+ it('should apply a hook for retreiving data from multiple selectors', () => {
+ const { result: errorResponse } = shallowHook(() =>
+ useGetScans({
+ useSelectorsResponse: () => ({ error: true, message: 'Lorem ipsum' })
+ })
+ );
+
+ const { result: pendingResponse } = shallowHook(() =>
+ useGetScans({
+ useSelectorsResponse: () => ({ pending: true })
+ })
+ );
+
+ const { result: fulfilledResponse } = shallowHook(() =>
+ useGetScans({
+ useSelectorsResponse: () => ({ fulfilled: true, data: { view: { results: ['dolor', 'sit'] } } })
+ })
+ );
+
+ const { result: mockStoreSuccessResponse } = shallowHook(() => useGetScans(), {
+ state: {
+ viewOptions: {
+ [reduxTypes.view.SCANS_VIEW]: {}
+ },
+ scans: {
+ expanded: {},
+ selected: {},
+ view: {
+ fulfilled: true,
+ data: {
+ results: ['lorem', 'ipsum']
+ }
+ }
+ }
+ }
+ });
+
+ expect({ errorResponse, fulfilledResponse, pendingResponse, mockStoreSuccessResponse }).toMatchSnapshot(
+ 'responses'
+ );
+ });
+});
diff --git a/src/components/scans/__tests__/scansTableCells.test.js b/src/components/scans/__tests__/scansTableCells.test.js
new file mode 100644
index 00000000..e15dd495
--- /dev/null
+++ b/src/components/scans/__tests__/scansTableCells.test.js
@@ -0,0 +1,11 @@
+import { scansTableCells } from '../scansTableCells';
+
+describe('ScansTableCells', () => {
+ it('should export specific function components', () => {
+ expect(scansTableCells).toMatchSnapshot('function components');
+ });
+
+ it('should return consistent cell results', () => {
+ Object.entries(scansTableCells).forEach(([key, value]) => expect(value()).toMatchSnapshot(`basic ${key} cell`));
+ });
+});
diff --git a/src/components/scans/scanDownload.js b/src/components/scans/scanDownload.js
index e4bce993..52a87d4b 100644
--- a/src/components/scans/scanDownload.js
+++ b/src/components/scans/scanDownload.js
@@ -21,7 +21,7 @@ class ScanDownload extends React.Component {
if (error) {
store.dispatch({
type: reduxTypes.toastNotifications.TOAST_ADD,
- alertType: 'error',
+ alertType: 'danger',
header: 'Error',
message: helpers.getMessageFromResults(results).message
});
@@ -49,7 +49,7 @@ class ScanDownload extends React.Component {
return (
- {tooltip && {button} }
+ {tooltip && {button} }
{!tooltip && button}
);
@@ -57,7 +57,7 @@ class ScanDownload extends React.Component {
}
ScanDownload.propTypes = {
- children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ children: PropTypes.node,
tooltip: PropTypes.string,
downloadId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
downloadName: PropTypes.string,
diff --git a/src/components/scans/scanJobsList.js b/src/components/scans/scanJobsList.js
index 9c1d6b5c..37f04edf 100644
--- a/src/components/scans/scanJobsList.js
+++ b/src/components/scans/scanJobsList.js
@@ -1,13 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
-import cx from 'classnames';
-import { EmptyState, Grid, Icon, Spinner } from 'patternfly-react';
+import { Alert, AlertVariant, EmptyState, EmptyStateVariant, Spinner } from '@patternfly/react-core';
+import { Grid } from 'patternfly-react';
+import { IconSize } from '@patternfly/react-icons';
import { connect, reduxActions, reduxSelectors } from '../../redux';
+import { ContextIcon, ContextIconColors, ContextIconVariant } from '../contextIcon/contextIcon';
import { helpers } from '../../common/helpers';
import { dictionary } from '../../constants/dictionaryConstants';
import { apiTypes } from '../../constants/apiConstants';
import ScanDownload from './scanDownload';
+import { translate } from '../i18n/i18n';
+/**
+ * Return a scan jobs listing for "jobs".
+ */
class ScanJobsList extends React.Component {
state = {
currentPage: 1,
@@ -51,36 +57,26 @@ class ScanJobsList extends React.Component {
};
render() {
- const { error, errorMessage, mostRecentId, pending, scanJobsList } = this.props;
+ const { error, errorMessage, mostRecentId, pending, scanJobsList, t } = this.props;
- if (error) {
+ if (pending) {
return (
-
-
- Error retrieving scan jobs
- {errorMessage}
+
+ {t('view.loading')}
);
}
- if (pending) {
+ if (error) {
return (
-
-
- Loading...
+
+
+ {t('view.error-message', { context: ['scan-jobs'], message: errorMessage })}
+
);
}
- const iconProps = status => {
- const { type, name, classNames } = helpers.scanStatusIcon(status);
- return {
- type,
- name,
- className: cx('scan-job-status-icon', classNames)
- };
- };
-
return (
@@ -89,7 +85,7 @@ class ScanJobsList extends React.Component {
mostRecentId !== item.id && (
-
+ {' '}
{dictionary[item.status] || ''}
@@ -98,17 +94,17 @@ class ScanJobsList extends React.Component {
)}
-
+ {' '}
{item.systemsScanned}
-
+ {' '}
{item.systemsFailed}
{item.reportId > 0 && (
- Download
+ Download
)}
@@ -121,6 +117,12 @@ class ScanJobsList extends React.Component {
}
}
+/**
+ * Prop types
+ *
+ * @type {{t: Function, isMoreResults: boolean, pending: boolean, errorMessage: string, getScanJobs: Function,
+ * scanJobsList: Array, id: string|number, error: boolean, mostRecentId: string|number}}
+ */
ScanJobsList.propTypes = {
error: PropTypes.bool,
errorMessage: PropTypes.string,
@@ -140,9 +142,16 @@ ScanJobsList.propTypes = {
systemsScanned: PropTypes.number,
systemsFailed: PropTypes.number
})
- )
+ ),
+ t: PropTypes.func
};
+/**
+ * Default props
+ *
+ * @type {{t: translate, isMoreResults: boolean, pending: boolean, errorMessage: null, getScanJobs: Function,
+ * scanJobsList: *[], error: boolean, mostRecentId: null}}
+ */
ScanJobsList.defaultProps = {
error: false,
errorMessage: null,
@@ -150,7 +159,8 @@ ScanJobsList.defaultProps = {
isMoreResults: false,
mostRecentId: null,
pending: false,
- scanJobsList: []
+ scanJobsList: [],
+ t: translate
};
const mapDispatchToProps = dispatch => ({
diff --git a/src/components/scans/scanListItem.js b/src/components/scans/scanListItem.js
deleted file mode 100644
index a23b0f8a..00000000
--- a/src/components/scans/scanListItem.js
+++ /dev/null
@@ -1,435 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import cx from 'classnames';
-import { Button, Checkbox, Grid, Icon, ListView } from 'patternfly-react';
-import { connect, reduxActions, reduxSelectors, reduxTypes, store } from '../../redux';
-import { helpers } from '../../common/helpers';
-import Tooltip from '../tooltip/tooltip';
-import ScanSourceList from './scanSourceList';
-import ScanHostList from '../scanHostList/scanHostList';
-import ScanJobsList from './scanJobsList';
-import ScanDownload from './scanDownload';
-import ListStatusItem from '../listStatusItem/listStatusItem';
-import Poll from '../poll/poll';
-import { apiTypes } from '../../constants/apiConstants';
-
-class ScanListItem extends React.Component {
- static notifyActionStatus(scan, actionText, error, results) {
- if (error) {
- store.dispatch({
- type: reduxTypes.toastNotifications.TOAST_ADD,
- alertType: 'error',
- header: 'Error',
- message: helpers.getMessageFromResults(results).message
- });
- } else {
- store.dispatch({
- type: reduxTypes.toastNotifications.TOAST_ADD,
- alertType: 'success',
- message: (
-
- Scan {scan.name} {actionText}.
-
- )
- });
- }
- }
-
- state = {
- expandType: null
- };
-
- onRefresh = () => {
- store.dispatch({
- type: reduxTypes.scans.UPDATE_SCANS
- });
- };
-
- onItemSelectChange = event => {
- const { checked } = event.target;
- const { scan } = this.props;
-
- store.dispatch({
- type: checked ? reduxTypes.view.SELECT_ITEM : reduxTypes.view.DESELECT_ITEM,
- viewType: reduxTypes.view.SCANS_VIEW,
- item: scan
- });
- };
-
- onToggleExpand = toggleExpandType => {
- const { expandType } = this.state;
- const toggle = expandType === toggleExpandType ? null : toggleExpandType;
-
- this.setState({ expandType: toggle });
- };
-
- onCloseExpand = () => {
- this.setState({ expandType: null });
- };
-
- onStartScan = () => {
- const { scan, startScan } = this.props;
-
- startScan(scan.id)
- .then(
- response => ScanListItem.notifyActionStatus(scan, 'started', false, response.value),
- error => ScanListItem.notifyActionStatus(scan, 'started', true, error)
- )
- .finally(() => this.onRefresh());
- };
-
- onPauseScan = () => {
- const { scan, pauseScan } = this.props;
-
- pauseScan(scan.mostRecentId)
- .then(
- response => ScanListItem.notifyActionStatus(scan, 'paused', false, response.value),
- error => ScanListItem.notifyActionStatus(scan, 'paused', true, error)
- )
- .finally(() => this.onRefresh());
- };
-
- onResumeScan = () => {
- const { scan, restartScan } = this.props;
-
- restartScan(scan.mostRecentId)
- .then(
- response => ScanListItem.notifyActionStatus(scan, 'resumed', false, response.value),
- error => ScanListItem.notifyActionStatus(scan, 'resumed', true, error)
- )
- .finally(() => this.onRefresh());
- };
-
- onCancelScan = () => {
- const { scan, cancelScan } = this.props;
-
- cancelScan(scan.mostRecentId)
- .then(
- response => ScanListItem.notifyActionStatus(scan, 'canceled', false, response.value),
- error => ScanListItem.notifyActionStatus(scan, 'canceled', true, error)
- )
- .finally(() => this.onRefresh());
- };
-
- isSelected() {
- const { scan, selectedScans } = this.props;
-
- return selectedScans.find(nextSelected => nextSelected[apiTypes.API_RESPONSE_SCAN_ID] === scan.id) !== undefined;
- }
-
- renderDescription() {
- const { scan } = this.props;
- const scanStatus = scan.mostRecentStatus;
- const statusIconInfo = helpers.scanStatusIcon(scanStatus);
-
- const icon = statusIconInfo ? (
-
- ) : null;
-
- let scanTime = scan.mostRecentEndTime;
-
- if (scanStatus === 'pending' || scanStatus === 'running') {
- scanTime = scan.mostRecentStartTime;
- }
-
- return (
-
- {icon}
-
-
{(scan.mostRecentStatusMessage && scan.mostRecentStatusMessage) || 'Scan created'}
-
{scanTime && helpers.getTimeDisplayHowLongAgo(scanTime)}
-
-
- );
- }
-
- renderStatusItems() {
- const { expandType } = this.state;
- const { scan } = this.props;
-
- const sourcesCount = scan.sourcesTotal;
- const prevCount = Math.max(scan.jobsTotal - 1, 0);
- const successHosts = scan.mostRecentSysScanned;
- const failedHosts = scan.mostRecentSysFailed;
-
- return [
- ,
- ,
- ,
-
- ];
- }
-
- renderActions() {
- const { scan } = this.props;
- const downloadActions = scan.mostRecentReportId && (
-
- );
-
- switch (scan.mostRecentStatus) {
- case 'completed':
- return (
-
-
- this.onStartScan(scan)} bsStyle="link">
-
-
-
- {downloadActions}
-
- );
- case 'failed':
- case 'canceled':
- return (
-
-
- this.onStartScan(scan)} bsStyle="link">
-
-
-
- {downloadActions}
-
- );
- case 'created':
- case 'running':
- return (
-
-
-
-
-
-
-
-
-
-
-
- {downloadActions}
-
- );
- case 'paused':
- return (
-
-
-
-
-
-
- {downloadActions}
-
- );
- case 'pending':
- return (
-
-
-
-
-
-
- {downloadActions}
-
- );
- default:
- return (
-
-
-
-
-
-
- {downloadActions}
-
- );
- }
- }
-
- static renderHostRow(host) {
- return (
-
-
-
-
- {host.name}
-
-
-
- {host.sourceName}
-
-
- );
- }
-
- renderExpansionContents() {
- const { expandType } = this.state;
- const { scan, lastRefresh } = this.props;
-
- switch (expandType) {
- case 'systemsScanned':
- return (
-
- {({ host }) => ScanListItem.renderHostRow(host)}
-
- );
- case 'systemsFailed':
- return (
-
- {({ host }) => ScanListItem.renderHostRow(host)}
-
- );
- case 'sources':
- return ;
- case 'jobs':
- return ;
- default:
- return null;
- }
- }
-
- render() {
- const { expandType } = this.state;
- const { pollInterval, scan } = this.props;
- const selected = this.isSelected();
-
- const classes = cx({
- 'quipucords-scan-list-item': true,
- 'list-view-pf-top-align': true,
- active: selected
- });
-
- return (
-
- }
- actions={this.renderActions()}
- leftContent={{scan.name}
}
- description={this.renderDescription()}
- additionalInfo={this.renderStatusItems()}
- compoundExpand
- compoundExpanded={expandType !== null}
- onCloseCompoundExpand={this.onCloseExpand}
- >
- {this.renderExpansionContents()}
-
-
- );
- }
-}
-
-ScanListItem.propTypes = {
- cancelScan: PropTypes.func,
- scan: PropTypes.shape({
- jobsTotal: PropTypes.number,
- id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- mostRecentEndTime: PropTypes.string,
- mostRecentId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- mostRecentReportId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- mostRecentStatus: PropTypes.string,
- mostRecentStartTime: PropTypes.string,
- mostRecentStatusMessage: PropTypes.string,
- mostRecentSysFailed: PropTypes.number,
- mostRecentSysScanned: PropTypes.number,
- name: PropTypes.string,
- sourcesTotal: PropTypes.number
- }).isRequired,
- lastRefresh: PropTypes.number,
- pollInterval: PropTypes.number,
- pauseScan: PropTypes.func,
- restartScan: PropTypes.func,
- selectedScans: PropTypes.array,
- startScan: PropTypes.func
-};
-
-ScanListItem.defaultProps = {
- cancelScan: helpers.noop,
- lastRefresh: 0,
- pollInterval: 120000,
- pauseScan: helpers.noop,
- restartScan: helpers.noop,
- selectedScans: [],
- startScan: helpers.noop
-};
-
-const mapDispatchToProps = dispatch => ({
- cancelScan: id => dispatch(reduxActions.scans.cancelScan(id)),
- pauseScan: id => dispatch(reduxActions.scans.pauseScan(id)),
- restartScan: id => dispatch(reduxActions.scans.restartScan(id)),
- startScan: id => dispatch(reduxActions.scans.startScan(id))
-});
-
-const makeMapStateToProps = () => {
- const getScanListItem = reduxSelectors.scans.makeScanListItem();
-
- return (state, props) => ({
- selectedScans: state.viewOptions[reduxTypes.view.SCANS_VIEW].selectedItems,
- ...getScanListItem(state, props)
- });
-};
-
-const ConnectedScanListItem = connect(makeMapStateToProps, mapDispatchToProps)(ScanListItem);
-
-export { ConnectedScanListItem as default, ConnectedScanListItem, ScanListItem };
diff --git a/src/components/scans/scanSourceList.js b/src/components/scans/scanSourceList.js
index 2bee8c9e..7bec76bc 100644
--- a/src/components/scans/scanSourceList.js
+++ b/src/components/scans/scanSourceList.js
@@ -1,9 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { EmptyState, Grid, Icon, Spinner } from 'patternfly-react';
+import { Alert, AlertVariant, EmptyState, EmptyStateVariant, List, ListItem, Spinner } from '@patternfly/react-core';
import { connect, reduxActions, reduxSelectors } from '../../redux';
+import { ContextIcon, ContextIconVariant } from '../contextIcon/contextIcon';
import { helpers } from '../../common/helpers';
+import { translate } from '../i18n/i18n';
+/**
+ * Return a scan jobs listing for "sources".
+ */
class ScanSourceList extends React.Component {
static setSourceStatus(source) {
if (!source.connectTaskStatus && !source.inspectTaskStatus) {
@@ -24,45 +29,49 @@ class ScanSourceList extends React.Component {
}
render() {
- const { error, errorMessage, pending, scanJobList } = this.props;
+ const { error, errorMessage, pending, scanJobList, t } = this.props;
- if (error) {
+ if (pending) {
return (
-
-
- Error retrieving scan jobs
- {errorMessage}
+
+ {t('view.loading')}
);
}
- if (pending) {
+ if (error) {
return (
-
-
- Loading...
+
+
+ {t('view.error-message', { context: ['scan-jobs'], message: errorMessage })}
+
);
}
return (
-
- {scanJobList.map(item => (
-
-
-
- {item.name}
-
-
- {ScanSourceList.setSourceStatus(item)}
-
-
+
+ {scanJobList?.map(item => (
+
+
+ } key={item.name}>
+ {item.name}
+
+ {ScanSourceList.setSourceStatus(item)}
+
+
))}
-
+
);
}
}
+/**
+ * Prop types
+ *
+ * @type {{t: Function, pending: boolean, errorMessage: string, getScanJob: Function, id: string|number,
+ * error: boolean, scanJobList: Array}}
+ */
ScanSourceList.propTypes = {
error: PropTypes.bool,
errorMessage: PropTypes.string,
@@ -79,15 +88,23 @@ ScanSourceList.propTypes = {
name: PropTypes.string,
sourceType: PropTypes.string
})
- )
+ ),
+ t: PropTypes.func
};
+/**
+ * Default props
+ *
+ * @type {{t: translate, pending: boolean, errorMessage: null, getScanJob: Function, error: boolean,
+ * scanJobList: *[]}}
+ */
ScanSourceList.defaultProps = {
error: false,
errorMessage: null,
getScanJob: helpers.noop,
pending: false,
- scanJobList: []
+ scanJobList: [],
+ t: translate
};
const mapDispatchToProps = dispatch => ({
diff --git a/src/components/scans/scans.js b/src/components/scans/scans.js
index b7b9007d..6f5401a2 100644
--- a/src/components/scans/scans.js
+++ b/src/components/scans/scans.js
@@ -1,197 +1,229 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Alert, Button, EmptyState, ListView, Modal, Spinner } from 'patternfly-react';
-import _isEqual from 'lodash/isEqual';
-import _size from 'lodash/size';
-import { connect, reduxActions, reduxSelectors, reduxTypes, store } from '../../redux';
-import helpers from '../../common/helpers';
+import { Alert, AlertVariant, Button, ButtonVariant, EmptyState, Spinner } from '@patternfly/react-core';
+import { IconSize } from '@patternfly/react-icons';
+import { Modal, ModalVariant } from '../modal/modal';
+import { reduxTypes, storeHooks } from '../../redux';
import ViewToolbar from '../viewToolbar/viewToolbar';
import ViewPaginationRow from '../viewPaginationRow/viewPaginationRow';
-import ScansEmptyState from './scansEmptyState';
-import ScanListItem from './scanListItem';
-import Tooltip from '../tooltip/tooltip';
+import { ScansEmptyState } from './scansEmptyState';
import { ScanFilterFields, ScanSortFields } from './scanConstants';
-import { apiTypes } from '../../constants/apiConstants';
-
-class Scans extends React.Component {
- componentDidMount() {
- const { getScans, viewOptions } = this.props;
-
- getScans(helpers.createViewQueryObject(viewOptions, { [apiTypes.API_QUERY_SCAN_TYPE]: 'inspect' }));
- }
-
- componentDidUpdate(prevProps) {
- const { getScans, update, viewOptions } = this.props;
-
- const prevQuery = helpers.createViewQueryObject(prevProps.viewOptions, {
- [apiTypes.API_QUERY_SCAN_TYPE]: 'inspect'
- });
- const nextQuery = helpers.createViewQueryObject(viewOptions, { [apiTypes.API_QUERY_SCAN_TYPE]: 'inspect' });
-
- if (update || !_isEqual(prevQuery, nextQuery)) {
- getScans(nextQuery);
- }
- }
-
- onMergeScanResults = () => {
- const { viewOptions } = this.props;
-
- store.dispatch({
+import { translate } from '../i18n/i18n';
+import { Table } from '../table/table';
+import { scansTableCells } from './scansTableCells';
+import { useGetScans, useOnExpand, useOnRefresh, useOnScanAction, useOnSelect } from './scansContext';
+import { Tooltip } from '../tooltip/tooltip';
+
+const VIEW_ID = 'scans';
+
+// ToDo: review onMergeReports, renderToolbarActions being standalone with upcoming toolbar updates
+// ToDo: review items being selected and the page polling. Randomized dev data gives the appearance of an issue. Also applies to sources selected items
+/**
+ * A scans view.
+ *
+ * @param {object} props
+ * @param {Function} props.t
+ * @param {Function} props.useGetScans
+ * @param {Function} props.useOnExpand
+ * @param {Function} props.useOnRefresh
+ * @param {Function} props.useOnScanAction
+ * @param {Function} props.useOnSelect
+ * @param {Function} props.useDispatch
+ * @param {Function} props.useSelectors
+ * @param {string} props.viewId
+ * @returns {React.ReactNode}
+ */
+const Scans = ({
+ t,
+ useGetScans: useAliasGetScans,
+ useOnExpand: useAliasOnExpand,
+ useOnRefresh: useAliasOnRefresh,
+ useOnScanAction: useAliasOnScanAction,
+ useOnSelect: useAliasOnSelect,
+ useDispatch: useAliasDispatch,
+ useSelectors: useAliasSelectors,
+ viewId
+}) => {
+ const dispatch = useAliasDispatch();
+ const onExpand = useAliasOnExpand();
+ const onRefresh = useAliasOnRefresh();
+ const { onCancel, onDownload, onPause, onRestart, onStart } = useAliasOnScanAction();
+ const onSelect = useAliasOnSelect();
+ const { pending, error, errorMessage, date, data, selectedRows = {}, expandedRows = {} } = useAliasGetScans();
+ const [viewOptions = {}] = useAliasSelectors([
+ ({ viewOptions: stateViewOptions }) => stateViewOptions[reduxTypes.view.SCANS_VIEW]
+ ]);
+ const isActive = viewOptions?.activeFilters?.length > 0 || data?.length > 0 || false;
+
+ /**
+ * Toolbar actions onScanSources
+ *
+ * @event onMergeReports
+ */
+ const onMergeReports = () => {
+ dispatch({
type: reduxTypes.scans.MERGE_SCAN_DIALOG_SHOW,
show: true,
- scans: viewOptions.selectedItems
+ scans: Object.values(selectedRows).filter(val => val !== null)
});
};
- onRefresh = () => {
- store.dispatch({
- type: reduxTypes.scans.UPDATE_SCANS
- });
- };
-
- onClearFilters = () => {
- store.dispatch({
- type: reduxTypes.viewToolbar.CLEAR_FILTERS,
- viewType: reduxTypes.view.SCANS_VIEW
- });
- };
-
- renderScansActions() {
- const { viewOptions } = this.props;
-
+ /**
+ * Return toolbar actions.
+ *
+ * @returns {React.ReactNode}
+ */
+ const renderToolbarActions = () => (
+
+ val !== null).length <= 1}
+ onClick={onMergeReports}
+ >
+ {t('table.label', { context: ['merge-reports'] })}
+
+
+ );
+
+ if (pending) {
return (
-
-
-
- Merge reports
-
-
-
+
+
+ {t('view.loading', { context: viewId })}
+
);
}
- renderPendingMessage() {
- const { pending } = this.props;
-
- if (pending) {
- return (
-
-
-
- Loading...
-
-
- );
- }
-
- return null;
- }
-
- renderScansList(scans) {
- const { lastRefresh } = this.props;
-
- if (scans.length) {
- return (
-
- {scans.map(scan => (
-
- ))}
-
- );
- }
-
+ if (error) {
return (
-
- No Results Match the Filter Criteria
- The active filters are hiding all items.
-
-
- Clear Filters
-
-
+
+
+ {t('view.error-message', { context: [viewId], message: errorMessage })}
+
);
}
- render() {
- const { error, errorMessage, lastRefresh, pending, scans, viewOptions } = this.props;
-
- if (error) {
- return (
-
-
- Error retrieving scans: {errorMessage}
-
- {this.renderPendingMessage()}
-
- );
- }
-
- if (pending && !scans.length) {
- return {this.renderPendingMessage()}
;
- }
-
- if (scans.length || _size(viewOptions.activeFilters)) {
- return (
-
+ return (
+
+ {isActive && (
+
onRefresh()}
+ lastRefresh={new Date(date).getTime()}
+ actions={renderToolbarActions()}
itemsType="Scan"
itemsTypePlural="Scans"
- selectedCount={viewOptions.selectedItems.length}
+ selectedCount={viewOptions.selectedItems?.length}
{...viewOptions}
/>
- {this.renderScansList(scans)}
- {this.renderPendingMessage()}
-
- );
- }
-
- return
;
- }
-}
+
+ )}
+
+
({
+ isSelected: (selectedRows?.[item.id] && true) || false,
+ item,
+ cells: [
+ {
+ content: scansTableCells.description(item),
+ dataLabel: t('table.header', { context: ['description'] })
+ },
+ {
+ content: scansTableCells.scanStatus(item, { viewId }),
+ width: 20,
+ dataLabel: t('table.header', { context: ['scan'] })
+ },
+ {
+ ...scansTableCells.okHostsCellContent(item, { viewId }),
+ isExpanded: expandedRows?.[item.id] === 2,
+ width: 8,
+ dataLabel: t('table.header', { context: ['success', viewId] })
+ },
+ {
+ ...scansTableCells.failedHostsCellContent(item, { viewId }),
+ isExpanded: expandedRows?.[item.id] === 3,
+ width: 8,
+ dataLabel: t('table.header', { context: ['failed', viewId] })
+ },
+ {
+ ...scansTableCells.sourcesCellContent(item, { viewId }),
+ isExpanded: expandedRows?.[item.id] === 4,
+ width: 8,
+ dataLabel: t('table.header', { context: ['sources'] })
+ },
+ {
+ ...scansTableCells.scansCellContent(item, { viewId }),
+ isExpanded: expandedRows?.[item.id] === 5,
+ width: 8,
+ dataLabel: t('table.header', { context: ['scan-jobs'] })
+ },
+ {
+ style: { textAlign: 'right' },
+ content: scansTableCells.actionsCell({
+ isFirst: index === 0,
+ isLast: index === data.length - 1,
+ item,
+ onCancel: () => onCancel(item),
+ onDownload: () => onDownload(item),
+ onRestart: () => onRestart(item),
+ onPause: () => onPause(item),
+ onStart: () => onStart(item)
+ }),
+ isActionCell: true
+ }
+ ]
+ }))}
+ >
+
+
+
+
+ );
+};
+/**
+ * Prop types
+ *
+ * @type {{useOnEdit: Function, useOnSelect: Function, viewId: string, t: Function, useOnRefresh: Function, useOnScan: Function,
+ * useDispatch: Function, useOnDelete: Function, useOnExpand: Function, useGetSources: Function, useSelectors: Function,
+ * useOnShowAddSourceWizard: Function}}
+ */
Scans.propTypes = {
- error: PropTypes.bool,
- errorMessage: PropTypes.string,
- getScans: PropTypes.func,
- lastRefresh: PropTypes.number,
- pending: PropTypes.bool,
- scans: PropTypes.array,
- update: PropTypes.bool,
- viewOptions: PropTypes.object
+ t: PropTypes.func,
+ useDispatch: PropTypes.func,
+ useGetScans: PropTypes.func,
+ useOnExpand: PropTypes.func,
+ useOnRefresh: PropTypes.func,
+ useOnScanAction: PropTypes.func,
+ useOnSelect: PropTypes.func,
+ useSelectors: PropTypes.func,
+ viewId: PropTypes.string
};
+/**
+ * Default props
+ *
+ * @type {{useOnEdit: Function, useOnSelect: Function, viewId: string, t: translate, useOnRefresh: Function, useOnScan: Function,
+ * useDispatch: Function, useOnDelete: Function, useOnExpand: Function, useGetSources: Function, useSelectors: Function,
+ * useOnShowAddSourceWizard: Function}}
+ */
Scans.defaultProps = {
- error: false,
- errorMessage: null,
- getScans: helpers.noop,
- lastRefresh: 0,
- pending: false,
- scans: [],
- update: false,
- viewOptions: {}
+ t: translate,
+ useDispatch: storeHooks.reactRedux.useDispatch,
+ useGetScans,
+ useOnExpand,
+ useOnRefresh,
+ useOnScanAction,
+ useOnSelect,
+ useSelectors: storeHooks.reactRedux.useSelectors,
+ viewId: VIEW_ID
};
-const mapDispatchToProps = dispatch => ({
- getScans: queryObj => dispatch(reduxActions.scans.getScans(queryObj))
-});
-
-const makeMapStateToProps = () => {
- const scansView = reduxSelectors.scans.makeScansView();
-
- return (state, props) => ({
- ...scansView(state, props),
- viewOptions: state.viewOptions[reduxTypes.view.SCANS_VIEW]
- });
-};
-
-const ConnectedScans = connect(makeMapStateToProps, mapDispatchToProps)(Scans);
-
-export { ConnectedScans as default, ConnectedScans, Scans };
+export { Scans as default, Scans, VIEW_ID };
diff --git a/src/components/scans/scansContext.js b/src/components/scans/scansContext.js
new file mode 100644
index 00000000..4daf4681
--- /dev/null
+++ b/src/components/scans/scansContext.js
@@ -0,0 +1,307 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { AlertVariant } from '@patternfly/react-core';
+import { useShallowCompareEffect } from 'react-use';
+import { reduxActions, reduxTypes, storeHooks } from '../../redux';
+import { useTimeout } from '../../hooks';
+import { apiTypes } from '../../constants/apiConstants';
+import { helpers } from '../../common';
+import { translate } from '../i18n/i18n';
+
+/**
+ * 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.scans.EXPANDED_SCAN : reduxTypes.scans.NOT_EXPANDED_SCAN,
+ viewType: reduxTypes.view.SCANS_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.scans.UPDATE_SCANS
+ });
+ };
+};
+
+/**
+ * Report/scan actions cancel, pause, restart, start, and download.
+ *
+ * @param {object} options
+ * @param {Function} options.cancelScan
+ * @param {Function} options.getReportsDownload
+ * @param {Function} options.pauseScan
+ * @param {Function} options.restartScan
+ * @param {Function} options.startScan
+ * @param {Function} options.t
+ * @param {Function} options.useDispatch
+ * @param {Function} options.useSelectorsResponse
+ * @returns {{onRestart: Function, onDownload: Function, onStart: Function, onCancel: Function, onPause: Function}}
+ */
+const useOnScanAction = ({
+ cancelScan = reduxActions.scans.cancelScan,
+ getReportsDownload = reduxActions.reports.getReportsDownload,
+ pauseScan = reduxActions.scans.pauseScan,
+ restartScan = reduxActions.scans.restartScan,
+ startScan = reduxActions.scans.startScan,
+ t = translate,
+ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch,
+ useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse
+} = {}) => {
+ const [updatedScan, setUpdatedScan] = useState({});
+ const { id: scanId, name: scanName, context: scanContext } = updatedScan;
+ const dispatch = useAliasDispatch();
+ const { data, error, fulfilled, pending } = useAliasSelectorsResponse(({ scans }) => scans?.action?.[scanId]);
+ const { errorMessage } = data?.[0] || {};
+
+ useEffect(() => {
+ if (scanId && !pending) {
+ const dispatchList = [];
+
+ if (fulfilled) {
+ dispatchList.push({
+ type: reduxTypes.toastNotifications.TOAST_ADD,
+ alertType: AlertVariant.success,
+ message: t(
+ 'toast-notifications.description',
+ { context: ['scan-report', scanContext], name: scanName || scanId },
+ [ ]
+ )
+ });
+ }
+
+ if (error) {
+ const isWarning = /already\sfinished/i.test(errorMessage);
+
+ dispatchList.push({
+ type: reduxTypes.toastNotifications.TOAST_ADD,
+ alertType: (isWarning && AlertVariant.warning) || AlertVariant.danger,
+ header: t('toast-notifications.title', { context: [(isWarning && 'warning') || 'error'] }),
+ message:
+ errorMessage || t('toast-notifications.description', { context: [(isWarning && 'warning') || 'error'] })
+ });
+ }
+
+ if (dispatchList.length) {
+ dispatch([
+ ...dispatchList,
+ {
+ type: reduxTypes.scans.UPDATE_SCANS
+ }
+ ]);
+
+ setUpdatedScan({});
+ }
+ }
+ }, [dispatch, error, errorMessage, fulfilled, pending, scanContext, scanId, scanName, t]);
+
+ /**
+ * onCancel for scanning
+ *
+ * @type {Function}
+ */
+ const onCancel = useCallback(
+ ({ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: mostRecent, [apiTypes.API_RESPONSE_SCAN_NAME]: name }) => {
+ const id = mostRecent[apiTypes.API_RESPONSE_SCAN_MOST_RECENT_ID];
+ cancelScan(id)(dispatch);
+ setUpdatedScan(() => ({ id, name, context: 'canceled' }));
+ },
+ [cancelScan, dispatch]
+ );
+
+ /**
+ * onDownload for reports
+ *
+ * @type {Function}
+ */
+ const onDownload = useCallback(
+ ({ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: mostRecent, [apiTypes.API_RESPONSE_SCAN_NAME]: name }) => {
+ const id = mostRecent[apiTypes.API_RESPONSE_SCAN_MOST_RECENT_REPORT_ID];
+ getReportsDownload(id)(dispatch);
+ setUpdatedScan(() => ({ id, name, context: 'download' }));
+ },
+ [getReportsDownload, dispatch]
+ );
+
+ /**
+ * onPause for scanning
+ *
+ * @type {Function}
+ */
+ const onPause = useCallback(
+ ({ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: mostRecent, [apiTypes.API_RESPONSE_SCAN_NAME]: name }) => {
+ const id = mostRecent[apiTypes.API_RESPONSE_SCAN_MOST_RECENT_ID];
+ pauseScan(id)(dispatch);
+ setUpdatedScan(() => ({ id, name, context: 'paused' }));
+ },
+ [pauseScan, dispatch]
+ );
+
+ /**
+ * onRestart for scanning
+ *
+ * @type {Function}
+ */
+ const onRestart = useCallback(
+ ({ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: mostRecent, [apiTypes.API_RESPONSE_SCAN_NAME]: name }) => {
+ const id = mostRecent[apiTypes.API_RESPONSE_SCAN_MOST_RECENT_ID];
+ restartScan(id)(dispatch);
+ setUpdatedScan(() => ({ id, name, context: 'restart' }));
+ },
+ [restartScan, dispatch]
+ );
+
+ /**
+ * onStart for scanning
+ *
+ * @type {Function}
+ */
+ const onStart = useCallback(
+ ({ [apiTypes.API_RESPONSE_SCAN_ID]: id, [apiTypes.API_RESPONSE_SCAN_NAME]: name }) => {
+ startScan(id)(dispatch);
+ setUpdatedScan(() => ({ id, name, context: 'play' }));
+ },
+ [startScan, dispatch]
+ );
+
+ return {
+ onCancel,
+ onDownload,
+ onPause,
+ onRestart,
+ onStart
+ };
+};
+
+/**
+ * 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.scans.SELECT_SCAN : reduxTypes.scans.DESELECT_SCAN,
+ viewType: reduxTypes.view.SCANS_VIEW,
+ item: data.item
+ });
+ };
+};
+
+/**
+ * Poll data for pending results.
+ *
+ * @param {object} options
+ * @param {number} options.pollInterval
+ * @param {Function} options.useSelector
+ * @param {Function} options.useTimeout
+ * @returns {Function}
+ */
+const usePoll = ({
+ pollInterval = helpers.POLL_INTERVAL,
+ useSelector: useAliasSelector = storeHooks.reactRedux.useSelector,
+ useTimeout: useAliasTimeout = useTimeout
+} = {}) => {
+ const updatedSources = useAliasSelector(({ scans }) => scans?.view?.data?.results, []);
+ const { update } = useAliasTimeout(() => {
+ const filteredSources = updatedSources.filter(
+ ({ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: mostRecent }) =>
+ mostRecent?.status === 'created' || mostRecent?.status === 'pending' || mostRecent?.status === 'running'
+ );
+
+ return filteredSources.length > 0;
+ }, pollInterval);
+
+ return update;
+};
+
+/**
+ * Get scans
+ *
+ * @param {object} options
+ * @param {Function} options.getScans
+ * @param {Function} options.useDispatch
+ * @param {Function} options.usePoll
+ * @param {Function} options.useSelectors
+ * @param {Function} options.useSelectorsResponse
+ * @returns {{date: *, data: *[], pending: boolean, errorMessage: null, fulfilled: boolean, selectedRows: *,
+ * expandedRows: *, error: boolean}}
+ */
+const useGetScans = ({
+ getScans = reduxActions.scans.getScans,
+ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch,
+ usePoll: useAliasPoll = usePoll,
+ useSelectors: useAliasSelectors = storeHooks.reactRedux.useSelectors,
+ useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse
+} = {}) => {
+ const dispatch = useAliasDispatch();
+ const pollUpdate = useAliasPoll();
+ const [refreshUpdate, selectedRows, expandedRows, viewOptions] = useAliasSelectors([
+ ({ scans }) => scans?.update,
+ ({ scans }) => scans?.selected,
+ ({ scans }) => scans?.expanded,
+ ({ viewOptions: stateViewOptions }) => stateViewOptions?.[reduxTypes.view.SCANS_VIEW]
+ ]);
+ const {
+ data: responseData,
+ error,
+ fulfilled,
+ message: errorMessage,
+ pending,
+ responses = {}
+ } = useAliasSelectorsResponse({ id: 'view', selector: ({ scans }) => scans?.view });
+
+ const [{ date } = {}] = responses?.list || [];
+ const { results: data = [] } = responseData?.view || {};
+ const query = helpers.createViewQueryObject(viewOptions, { [apiTypes.API_QUERY_SCAN_TYPE]: 'inspect' });
+
+ useShallowCompareEffect(() => {
+ getScans(query)(dispatch);
+ }, [dispatch, getScans, pollUpdate, query, refreshUpdate]);
+
+ return {
+ pending,
+ error,
+ errorMessage,
+ fulfilled,
+ data,
+ date,
+ selectedRows,
+ expandedRows
+ };
+};
+
+const context = {
+ useGetScans,
+ useOnExpand,
+ useOnRefresh,
+ useOnScanAction,
+ useOnSelect,
+ usePoll
+};
+
+export { context as default, context, useGetScans, useOnExpand, useOnRefresh, useOnScanAction, useOnSelect, usePoll };
diff --git a/src/components/scans/scansEmptyState.js b/src/components/scans/scansEmptyState.js
index 2aa99d68..f1dec182 100644
--- a/src/components/scans/scansEmptyState.js
+++ b/src/components/scans/scansEmptyState.js
@@ -1,10 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Button, EmptyState, Grid, Row } from 'patternfly-react';
-import SourcesEmptyState from '../sources/sourcesEmptyState';
+import {
+ Button,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ EmptyStatePrimary,
+ EmptyStateVariant,
+ Title
+} from '@patternfly/react-core';
+import { AddCircleOIcon } from '@patternfly/react-icons';
import helpers from '../../common/helpers';
import { connectRouter, reduxActions, reduxSelectors, reduxTypes, store } from '../../redux';
+import { translate } from '../i18n/i18n';
+/**
+ * Return a scans empty state.
+ */
class ScansEmptyState extends React.Component {
componentDidMount() {
const { getScansSources } = this.props;
@@ -25,43 +37,59 @@ class ScansEmptyState extends React.Component {
};
render() {
- const { sourcesExist } = this.props;
+ const { sourcesCount, t, uiShortName, viewId } = this.props;
- if (sourcesExist) {
- return (
-
-
-
-
- No scans exist yet
- Select a Source to scan from the Sources page.
-
-
- Go to Sources
-
-
-
-
-
- );
- }
-
- return ;
+ return (
+
+
+
+ {t('view.empty-state', { context: ['title', viewId], count: sourcesCount, name: uiShortName })}
+
+
+ {t('view.empty-state', { context: ['description', viewId], count: sourcesCount })}
+
+
+
+ {t('view.empty-state', { context: ['label', 'source-navigate'], count: sourcesCount })}
+
+
+
+ );
}
}
+/**
+ * Prop types
+ *
+ * @type {{getScansSources: Function, uiShortName: string, sourcesExist: boolean, viewId: string, t: translate,
+ * history: object, sourcesCount: number}}
+ */
ScansEmptyState.propTypes = {
getScansSources: PropTypes.func,
history: PropTypes.shape({
push: PropTypes.func
}),
- sourcesExist: PropTypes.bool
+ sourcesCount: PropTypes.number,
+ sourcesExist: PropTypes.bool,
+ t: PropTypes.func,
+ uiShortName: PropTypes.string,
+ viewId: PropTypes.string
};
+/**
+ * Default props
+ *
+ * @type {{getScansSources: Function, uiShortName: string, sourcesExist: boolean, viewId: null, t: translate,
+ * history: {}, sourcesCount: number}}
+ */
ScansEmptyState.defaultProps = {
getScansSources: helpers.noop,
history: {},
- sourcesExist: false
+ sourcesCount: 0,
+ sourcesExist: false,
+ t: translate,
+ uiShortName: helpers.UI_SHORT_NAME,
+ viewId: null
};
const mapDispatchToProps = dispatch => ({
diff --git a/src/components/scans/scansTableCells.js b/src/components/scans/scansTableCells.js
new file mode 100644
index 00000000..193c4248
--- /dev/null
+++ b/src/components/scans/scansTableCells.js
@@ -0,0 +1,376 @@
+import React from 'react';
+import {
+ Button,
+ ButtonVariant,
+ Grid,
+ GridItem,
+ OverflowMenu,
+ OverflowMenuControl,
+ OverflowMenuContent,
+ OverflowMenuGroup,
+ OverflowMenuItem
+} from '@patternfly/react-core';
+import { EllipsisVIcon } from '@patternfly/react-icons';
+import { ContextIcon, ContextIconVariant } from '../contextIcon/contextIcon';
+import { ContextIconAction, ContextIconActionVariant } from '../contextIcon/contextIconAction';
+import { Tooltip } from '../tooltip/tooltip';
+import { ConnectedScanHostList as ScanHostList } from '../scanHostList/scanHostList';
+import { apiTypes } from '../../constants/apiConstants';
+import { translate } from '../i18n/i18n';
+import { helpers } from '../../common';
+import { DropdownSelect, SelectButtonVariant, SelectDirection, SelectPosition } from '../dropdownSelect/dropdownSelect';
+import ScanSourceList from './scanSourceList';
+import ScanJobsList from './scanJobsList';
+
+/**
+ * Source description and type icon
+ *
+ * @param {object} params
+ * @returns {React.ReactNode}
+ */
+const description = ({ [apiTypes.API_RESPONSE_SCAN_ID]: id, [apiTypes.API_RESPONSE_SCAN_NAME]: name } = {}) => (
+
+
+
+
+ {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 scanStatus = (
+ { [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: mostRecent = {} } = {},
+ { t = translate, viewId } = {}
+) => {
+ const {
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_STATUS]: status,
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_END_TIME]: endTime,
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_START_TIME]: startTime
+ } = mostRecent;
+ const isPending = status === 'created' || status === 'pending' || status === 'running';
+ const scanTime = (isPending && startTime) || endTime;
+
+ return (
+
+
+
+
+
+ {t('table.label', { context: ['status', status, viewId] })}
+ {helpers.getTimeDisplayHowLongAgo(scanTime)}
+
+
+ );
+};
+
+/**
+ * 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 a consistent display row for expandable content.
+ *
+ * @param {object} params
+ * @param {string} params.id
+ * @param {string} params.status
+ * @param {object} options
+ * @param {boolean} options.useConnectionResults
+ * @param {boolean} options.useInspectionResults
+ * @returns {React.ReactNode}
+ */
+const statusContent = ({ id, status } = {}, { useConnectionResults = false, useInspectionResults = false } = {}) => (
+
+ {({ host }) => (
+
+
+ {host?.name}
+
+
+ {host?.sourceName}
+
+
+ )}
+
+);
+/**
+ * Failed hosts cell and expandable content.
+ *
+ * @param {object} params
+ * @param {object} options
+ * @param {string} options.viewId
+ * @returns {{cell: React.ReactNode, content: React.ReactNode}}
+ */
+const failedHostsCellContent = (
+ { [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: mostRecent = {} } = {},
+ { viewId } = {}
+) => {
+ const {
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_SYS_FAILED]: systemsScanned,
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_ID]: mostRecentId
+ } = mostRecent;
+ const count = Number.parseInt(systemsScanned, 10);
+
+ return {
+ content: statusCell({ count, status: ContextIconVariant.failed, viewId }),
+ expandedContent:
+ (count &&
+ statusContent({ id: mostRecentId, status: ContextIconVariant.failed }, { useInspectionResults: true })) ||
+ undefined
+ };
+};
+
+/**
+ * Ok hosts cell and expandable content.
+ *
+ * @param {object} params
+ * @param {object} options
+ * @param {string} options.viewId
+ * @returns {{cell: React.ReactNode, content: React.ReactNode}}
+ */
+const okHostsCellContent = ({ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: mostRecent = {} } = {}, { viewId } = {}) => {
+ const {
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_SYS_SCANNED]: systemsScanned,
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_ID]: mostRecentId
+ } = mostRecent;
+ const count = Number.parseInt(systemsScanned, 10);
+
+ return {
+ content: statusCell({ count, status: ContextIconVariant.success, viewId }),
+ expandedContent:
+ (count &&
+ statusContent({ id: mostRecentId, status: ContextIconVariant.success }, { useInspectionResults: true })) ||
+ undefined
+ };
+};
+
+const sourcesCellContent = (
+ { [apiTypes.API_RESPONSE_SCAN_ID]: id, [apiTypes.API_RESPONSE_SCAN_SOURCES]: sources = [] } = {},
+ { viewId } = {}
+) => {
+ const count = sources?.length;
+
+ return {
+ content: statusCell({ count, status: 'sources', viewId }),
+ expandedContent: (count && ) || undefined
+ };
+};
+
+const scansCellContent = (
+ {
+ [apiTypes.API_RESPONSE_SCAN_ID]: id,
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: mostRecent = {},
+ [apiTypes.API_RESPONSE_SCAN_JOBS]: scanJobs = []
+ } = {},
+ { viewId } = {}
+) => {
+ const { [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_ID]: mostRecentId } = mostRecent;
+ const count = scanJobs?.length;
+
+ return {
+ content: statusCell({ count, status: 'scans', viewId }),
+ expandedContent: (count > 1 && ) || undefined
+ };
+};
+
+// FixMe: PF Overflow menu is attempting state updates on unmounted components
+/**
+ * FixMe: Older issue associated with displaying both "pause" and "cancel" after a user restarts a scan.
+ * Basically, in testing you can't immediately pause a scan, or multiple scans, or the API throws an error.
+ * Because of this we only display the "cancel"/"stop" button. Or at least that's what the old code was
+ * doing, pre-refactor. Hitting refresh should display both buttons. If this gets fixed the condition to
+ * modify is associated with...
+ * mostRecentStatus === 'created' || mostRecentStatus === 'running'
+ */
+/**
+ * Action cell content
+ *
+ * @param {object} params
+ * @param {boolean} params.isFirst
+ * @param {boolean} params.isLast
+ * @param {object} params.item
+ * @param {Function} params.onCancel
+ * @param {Function} params.onDownload
+ * @param {Function} params.onRestart
+ * @param {Function} params.onPause
+ * @param {Function} params.onStart
+ * @param {Function} params.t
+ * @returns {React.ReactNode}
+ */
+const actionsCell = ({
+ isFirst = false,
+ isLast = false,
+ item = {},
+ onCancel = helpers.noop,
+ onDownload = helpers.noop,
+ onRestart = helpers.noop,
+ onPause = helpers.noop,
+ onStart = helpers.noop,
+ t = translate
+} = {}) => {
+ const { [apiTypes.API_RESPONSE_SCAN_MOST_RECENT]: scan = {} } = item;
+ const {
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_REPORT_ID]: mostRecentReportId,
+ [apiTypes.API_RESPONSE_SCAN_MOST_RECENT_STATUS]: mostRecentStatus
+ } = scan;
+
+ /**
+ * Determine the correct context action to use for both dropdown,
+ * and callbacks.
+ *
+ * @param {object} event
+ * @param {string} event.value
+ * @returns {void}
+ */
+ const onSelect = ({ value }) => {
+ switch (value) {
+ case 'download':
+ return onDownload(item);
+ case 'created':
+ case 'running':
+ return onPause(item);
+ case 'paused':
+ return onRestart(item);
+ case 'pending':
+ return onCancel(item);
+ case 'completed':
+ case 'failed':
+ case 'canceled':
+ case 'cancelled':
+ default:
+ return onStart(item);
+ }
+ };
+
+ /**
+ * Generate a consistent menu item.
+ *
+ * @param {string} context
+ * @returns {React.ReactNode}
+ */
+ const menuItem = context => ({
+ dropdownMenuItem: { title: t('table.label', { context: ['action', 'scan', context] }), value: context },
+ overflowMenuItem: (
+
+
+ onSelect({ value: context })}
+ aria-label={t('table.label', { context: ['action', 'scan', context] })}
+ variant={ButtonVariant.plain}
+ >
+
+
+
+
+ )
+ });
+
+ const menuItems = [];
+
+ if (mostRecentStatus) {
+ if (mostRecentStatus === 'created' || mostRecentStatus === 'running') {
+ menuItems.push(menuItem(ContextIconActionVariant.running), menuItem(ContextIconActionVariant.pending));
+ } else {
+ menuItems.push(menuItem(mostRecentStatus));
+ }
+ }
+
+ if (mostRecentReportId) {
+ menuItems.push({
+ dropdownMenuItem: { title: t('table.label', { context: ['action', 'scan', 'download'] }), value: 'download' },
+ overflowMenuItem: (
+
+ onSelect({ value: 'download' })} variant={ButtonVariant.secondary}>
+ {t('table.label', { context: ['action', 'scan', 'download'] })}
+
+
+ )
+ });
+ }
+
+ return (
+
+
+
+ {menuItems.map(({ overflowMenuItem }) => overflowMenuItem)}
+
+
+
+ }
+ options={menuItems.map(({ dropdownMenuItem }) => dropdownMenuItem)}
+ />
+
+
+ );
+};
+
+const scansTableCells = {
+ actionsCell,
+ description,
+ failedHostsCellContent,
+ okHostsCellContent,
+ scanStatus,
+ scansCellContent,
+ sourcesCellContent,
+ statusCell,
+ statusContent
+};
+
+export {
+ scansTableCells as default,
+ scansTableCells,
+ actionsCell,
+ description,
+ failedHostsCellContent,
+ okHostsCellContent,
+ scanStatus,
+ scansCellContent,
+ sourcesCellContent,
+ statusCell,
+ statusContent
+};
diff --git a/src/components/sources/__tests__/__snapshots__/sourceCredentialsList.test.js.snap b/src/components/sources/__tests__/__snapshots__/sourceCredentialsList.test.js.snap
deleted file mode 100644
index f6e3d913..00000000
--- a/src/components/sources/__tests__/__snapshots__/sourceCredentialsList.test.js.snap
+++ /dev/null
@@ -1,38 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`SourceCredentialsList Component should render a sorted list 1`] = `
-
-`;
diff --git a/src/components/sources/__tests__/__snapshots__/sourceListItem.test.js.snap b/src/components/sources/__tests__/__snapshots__/sourceListItem.test.js.snap
deleted file mode 100644
index 9ce3cb62..00000000
--- a/src/components/sources/__tests__/__snapshots__/sourceListItem.test.js.snap
+++ /dev/null
@@ -1,150 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`SourceListItem Component should render a connected component with default props: connected 1`] = `
-
-`;
-
-exports[`SourceListItem Component should render a non-connected component: non-connected 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Scan
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0 Credentials
-
-
-
-
-
-
- 0 Successful
-
-
-
-
-
-
- 0 Failed
-
-
-
-
-
-
- 0 Unreachable
-
-
-
-
-
-
-
-`;
diff --git a/src/components/sources/__tests__/__snapshots__/sources.test.js.snap b/src/components/sources/__tests__/__snapshots__/sources.test.js.snap
index a8912940..3329655b 100644
--- a/src/components/sources/__tests__/__snapshots__/sources.test.js.snap
+++ b/src/components/sources/__tests__/__snapshots__/sources.test.js.snap
@@ -1,108 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Sources Component should render a connected component with default props: connected 1`] = ` `;
-
-exports[`Sources Component should render a non-connected component error: error 1`] = `
-
-
-
-
- Error retrieving sources:
-
-
-
+ t(view.error-message, {"context":"sources"})
+
+
`;
-exports[`Sources Component should render a non-connected component pending: pending 1`] = `
-
-`;
-
-exports[`Sources Component should render a non-connected component with empty state: empty state 1`] = `
-
-
-
-
-
-
-
- Welcome to Quipucords
-
-
- Begin by adding one or more sources. A source contains a collection of network information,
-
- including systems management solution information, IP addresses, or host names, in addition to
-
- SSH ports and credentials.
-
-
-
- Add Source
-
-
-
-
-
-`;
-
-exports[`Sources Component should render a non-connected component: non-connected 1`] = `
+exports[`Sources Component should handle multiple display states, pending, error, fulfilled: fulfilled 1`] = `
+
- Add
+ t(table.label, {"context":"add"})
+
- Scan
+ t(table.label, {"context":"scan"})
-
+
}
activeFilters={Array []}
filterFields={
@@ -145,10 +76,9 @@ exports[`Sources Component should render a non-connected component: non-connecte
filterValue=""
itemsType="Source"
itemsTypePlural="Sources"
- lastRefresh={0}
+ lastRefresh={NaN}
onRefresh={[Function]}
selectedCount={0}
- selectedItems={Array []}
sortAscending={true}
sortFields={
Array [
@@ -177,28 +107,430 @@ exports[`Sources Component should render a non-connected component: non-connecte
-
-
+
+
+
+
+
+
+
+
+ lorem
+
+
+
+ ,
+ "dataLabel": "t(table.header, {\\"context\\":\\"description\\"})",
+ },
+ Object {
+ "content":
+
+
+
+
+
+ t(table.label_status, {"context":"sources"})
+
+ a day ago
+
+ ,
+ "dataLabel": "t(table.header, {\\"context\\":\\"scan\\"})",
+ "width": 20,
+ },
+ Object {
+ "content":
+ t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object])
+ ,
+ "dataLabel": "t(table.header, {\\"context\\":\\"credentials\\"})",
+ "expandedContent": undefined,
+ "isExpanded": false,
+ "width": 8,
+ },
+ Object {
+ "content":
+ t(table.label_status_cell, {"context":"sources","count":0}, [object Object],[object Object])
+ ,
+ "dataLabel": "t(table.header_success, {\\"context\\":\\"sources\\"})",
+ "expandedContent": undefined,
+ "isExpanded": false,
+ "width": 8,
+ },
+ Object {
+ "content":
+ t(table.label_status_cell, {"context":"sources","count":0}, [object Object],[object Object])
+ ,
+ "dataLabel": "t(table.header_failed, {\\"context\\":\\"sources\\"})",
+ "expandedContent": undefined,
+ "isExpanded": false,
+ "width": 8,
+ },
+ Object {
+ "content":
+ t(table.label_status_cell, {"context":"sources","count":0}, [object Object],[object Object])
+ ,
+ "dataLabel": "t(table.header_unreachable, {\\"context\\":\\"sources\\"})",
+ "expandedContent": undefined,
+ "isExpanded": false,
+ "width": 8,
+ },
+ Object {
+ "content":
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ t(table.label, {"context":"scan"})
+
+
+
+
+
+
+ }
+ position="right"
+ selectedOptions={null}
+ splitButtonVariant={null}
+ toggleIcon={null}
+ variant="single"
+ />
+
+ ,
+ "isActionCell": true,
+ },
+ ],
+ "isSelected": false,
+ "source": Object {
+ "id": "1",
+ "name": "lorem",
+ },
+ },
+ ]
+ }
+ summary={null}
+ variant="compact"
+ >
+
+
+
+
+`;
+
+exports[`Sources Component should handle multiple display states, pending, error, fulfilled: pending 1`] = `
+
+
+
+ t(view.loading, {"context":"sources"})
+
+
+`;
+
+exports[`Sources Component should render a basic component: basic 1`] = `
+
+`;
+
+exports[`Sources Component should return an empty state when there are no sources: empty state 1`] = `
+
+
+
+
+
+
+
+
+
+
+ t(view.empty-state, {"context":"title","name":"Quipucords"})
+
+
+ t(view.empty-state_description, {"context":"sources"})
+
+
+
+ t(view.empty-state_label, {"context":"sources"})
+
+
+
+
+
+
`;
diff --git a/src/components/sources/__tests__/__snapshots__/sourcesContext.test.js.snap b/src/components/sources/__tests__/__snapshots__/sourcesContext.test.js.snap
new file mode 100644
index 00000000..de6b61de
--- /dev/null
+++ b/src/components/sources/__tests__/__snapshots__/sourcesContext.test.js.snap
@@ -0,0 +1,133 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SourcesContext should apply a hook for retreiving 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[`SourcesContext should attempt to poll sources: timeout 1`] = `
+Array [
+ Array [
+ Object {
+ "callback": true,
+ "interval": 0,
+ },
+ ],
+ Array [
+ Object {
+ "callback": false,
+ "interval": 0,
+ },
+ ],
+]
+`;
+
+exports[`SourcesContext should handle deleting a source with a confirmation: dispatch delete a source, confirmation 1`] = `
+Array [
+ Array [
+ Object {
+ "confirmButtonText": "t(form-dialog.label, {\\"context\\":\\"delete\\"})",
+ "heading": "t(form-dialog.confirmation_heading, {\\"context\\":\\"delete-source\\",\\"name\\":\\"lorem ipsum name\\"}, [object Object])",
+ "onConfirm": [Function],
+ "title": "t(form-dialog.confirmation_title, {\\"context\\":\\"delete-source\\"})",
+ "type": "CONFIRMATION_MODAL_SHOW",
+ },
+ ],
+ Array [
+ Array [
+ Object {
+ "type": "CONFIRMATION_MODAL_HIDE",
+ },
+ ],
+ ],
+ Array [
+ Object {
+ "payload": Promise {},
+ "type": "DELETE_SOURCE",
+ },
+ ],
+ Array [
+ Array [
+ Object {
+ "alertType": "success",
+ "header": "t(toast-notifications.title, {\\"context\\":\\"deleted-source\\"})",
+ "message": "t(toast-notifications.description, {\\"context\\":\\"deleted-source\\",\\"name\\":\\"lorem ipsum name\\"})",
+ "type": "TOAST_ADD",
+ },
+ Object {
+ "type": "RESET_DELETE_SOURCE",
+ },
+ Object {
+ "item": Object {
+ "id": "dolor sit id",
+ "name": "lorem ipsum name",
+ },
+ "type": "DESELECT_ITEM",
+ "viewType": "SOURCES_VIEW",
+ },
+ Object {
+ "type": "UPDATE_SOURCES",
+ },
+ ],
+ ],
+]
+`;
+
+exports[`SourcesContext should return specific properties: specific properties 1`] = `
+Object {
+ "useGetSources": [Function],
+ "useOnDelete": [Function],
+ "useOnEdit": [Function],
+ "useOnExpand": [Function],
+ "useOnRefresh": [Function],
+ "useOnScan": [Function],
+ "useOnSelect": [Function],
+ "usePoll": [Function],
+}
+`;
diff --git a/src/components/sources/__tests__/__snapshots__/sourcesEmptyState.test.js.snap b/src/components/sources/__tests__/__snapshots__/sourcesEmptyState.test.js.snap
index 882d0e83..4bb230c2 100644
--- a/src/components/sources/__tests__/__snapshots__/sourcesEmptyState.test.js.snap
+++ b/src/components/sources/__tests__/__snapshots__/sourcesEmptyState.test.js.snap
@@ -2,60 +2,67 @@
exports[`SourcesEmptyState Component should render a basic component 1`] = `
+
+
+
+
+ t(view.empty-state, {"context":"title","name":"Quipucords"})
+
-
-
-
-
- Welcome to Quipucords
-
-
- Begin by adding one or more sources. A source contains a collection of network information,
-
- including systems management solution information, IP addresses, or host names, in addition to
-
- SSH ports and credentials.
-
-
+
+
-
- Add Source
-
-
+ t(view.empty-state, {"context":"label"})
+
`;
exports[`SourcesEmptyState Component should render the application name: application name 1`] = `
-
-
- Welcome to
- Ipsum
-
-
+ t(view.empty-state, {"context":"title","name":"Ipsum"})
+
+
`;
diff --git a/src/components/sources/__tests__/__snapshots__/sourcesTableCells.test.js.snap b/src/components/sources/__tests__/__snapshots__/sourcesTableCells.test.js.snap
new file mode 100644
index 00000000..5b169600
--- /dev/null
+++ b/src/components/sources/__tests__/__snapshots__/sourcesTableCells.test.js.snap
@@ -0,0 +1,264 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SourcesTableCells should export specific function components: function components 1`] = `
+Object {
+ "actionsCell": [Function],
+ "credentialsCellContent": [Function],
+ "description": [Function],
+ "failedHostsCellContent": [Function],
+ "okHostsCellContent": [Function],
+ "scanStatus": [Function],
+ "statusCell": [Function],
+ "statusContent": [Function],
+ "unreachableHostsCellContent": [Function],
+}
+`;
+
+exports[`SourcesTableCells should return consistent cell results: basic actionsCell cell 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ t(table.label, {"context":"scan"})
+
+
+
+
+
+
+ }
+ position="right"
+ selectedOptions={null}
+ splitButtonVariant={null}
+ toggleIcon={null}
+ variant="single"
+ />
+
+
+`;
+
+exports[`SourcesTableCells should return consistent cell results: basic credentialsCellContent cell 1`] = `
+Object {
+ "content":
+ t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object])
+ ,
+ "expandedContent": undefined,
+}
+`;
+
+exports[`SourcesTableCells should return consistent cell results: basic description cell 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`SourcesTableCells should return consistent cell results: basic failedHostsCellContent cell 1`] = `
+Object {
+ "content":
+ t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object])
+ ,
+ "expandedContent": undefined,
+}
+`;
+
+exports[`SourcesTableCells should return consistent cell results: basic okHostsCellContent cell 1`] = `
+Object {
+ "content":
+ t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object])
+ ,
+ "expandedContent": undefined,
+}
+`;
+
+exports[`SourcesTableCells should return consistent cell results: basic scanStatus cell 1`] = `
+
+
+
+
+
+
+ t(table.label, {"context":"status"})
+
+ a day ago
+
+
+`;
+
+exports[`SourcesTableCells should return consistent cell results: basic statusCell cell 1`] = `
+
+ t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object])
+
+`;
+
+exports[`SourcesTableCells should return consistent cell results: basic statusContent cell 1`] = `
+
+ [Function]
+
+`;
+
+exports[`SourcesTableCells should return consistent cell results: basic unreachableHostsCellContent cell 1`] = `
+Object {
+ "content":
+ t(table.label_status, {"context":"cell","count":0}, [object Object],[object Object])
+ ,
+ "expandedContent": undefined,
+}
+`;
diff --git a/src/components/sources/__tests__/sourceCredentialsList.test.js b/src/components/sources/__tests__/sourceCredentialsList.test.js
deleted file mode 100644
index e7fce687..00000000
--- a/src/components/sources/__tests__/sourceCredentialsList.test.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import SourceCredentialsList from '../sourceCredentialsList';
-
-describe('SourceCredentialsList Component', () => {
- it('should render a sorted list', () => {
- const props = {
- source: {
- credentials: [
- {
- name: 'a test'
- },
- {
- name: 'b test'
- }
- ]
- }
- };
-
- const component = mount(
);
-
- expect(component.render()).toMatchSnapshot();
- });
-});
diff --git a/src/components/sources/__tests__/sourceListItem.test.js b/src/components/sources/__tests__/sourceListItem.test.js
deleted file mode 100644
index d0f95d94..00000000
--- a/src/components/sources/__tests__/sourceListItem.test.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import React from 'react';
-import configureMockStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
-import { mount, shallow } from 'enzyme';
-import { ConnectedSourceListItem, SourceListItem } from '../sourceListItem';
-import { reduxTypes } from '../../../redux';
-import { apiTypes } from '../../../constants/apiConstants';
-
-describe('SourceListItem Component', () => {
- const generateEmptyStore = (obj = {}) => configureMockStore()(obj);
-
- it('should render a connected component with default props', () => {
- const store = generateEmptyStore({ sources: { view: {} }, viewOptions: { [reduxTypes.view.SOURCES_VIEW]: {} } });
- const props = {
- item: {
- [apiTypes.API_RESPONSE_SOURCE_ID]: 1,
- [apiTypes.API_RESPONSE_SOURCE_SOURCE_TYPE]: 'network'
- }
- };
- const component = shallow(
-
- {' '}
-
- );
-
- expect(component.find(ConnectedSourceListItem)).toMatchSnapshot('connected');
- });
-
- it('should render a non-connected component', () => {
- const props = {
- item: {
- [apiTypes.API_RESPONSE_SOURCE_ID]: 1,
- [apiTypes.API_RESPONSE_SOURCE_SOURCE_TYPE]: 'network'
- }
- };
-
- const component = mount(
);
-
- expect(component.render()).toMatchSnapshot('non-connected');
- });
-});
diff --git a/src/components/sources/__tests__/sources.test.js b/src/components/sources/__tests__/sources.test.js
index 8f0c9737..49633ce0 100644
--- a/src/components/sources/__tests__/sources.test.js
+++ b/src/components/sources/__tests__/sources.test.js
@@ -1,70 +1,64 @@
import React from 'react';
-import configureMockStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
-import { mount, shallow } from 'enzyme';
-import { ConnectedSources, Sources } from '../sources';
+import { Sources } from '../sources';
import { apiTypes } from '../../../constants/apiConstants';
describe('Sources Component', () => {
- const generateEmptyStore = (obj = {}) => configureMockStore()(obj);
-
- it('should render a connected component with default props', () => {
- const store = generateEmptyStore({ sources: { view: {} }, viewOptions: {} });
- const component = shallow(
-
-
-
- );
-
- expect(component.find(ConnectedSources)).toMatchSnapshot('connected');
- });
-
- it('should render a non-connected component', () => {
+ it('should render a basic component', async () => {
const props = {
- fulfilled: true,
- sources: [
- {
- [apiTypes.API_RESPONSE_SOURCE_ID]: '1',
- [apiTypes.API_RESPONSE_SOURCE_NAME]: 'lorem'
- }
- ],
- viewOptions: {
- selectedItems: []
- }
+ useGetSources: () => ({
+ fulfilled: true
+ })
};
- const component = shallow(
);
-
- expect(component).toMatchSnapshot('non-connected');
+ const component = await shallowHookComponent(
);
+ expect(component).toMatchSnapshot('basic');
});
- it('should render a non-connected component error', () => {
+ it('should handle multiple display states, pending, error, fulfilled', async () => {
const props = {
- error: true
+ useGetSources: () => ({
+ pending: true
+ })
};
- const component = shallow(
);
+ const component = await shallowHookComponent(
);
+ expect(component).toMatchSnapshot('pending');
- expect(component.render()).toMatchSnapshot('error');
- });
+ component.setProps({
+ useGetSources: () => ({
+ pending: false,
+ error: true
+ })
+ });
- it('should render a non-connected component pending', () => {
- const props = {
- pending: true,
- sources: []
- };
+ expect(component).toMatchSnapshot('error');
- const component = mount(
);
+ component.setProps({
+ useGetSources: () => ({
+ pending: false,
+ error: false,
+ fulfilled: true,
+ data: [
+ {
+ [apiTypes.API_RESPONSE_SOURCE_ID]: '1',
+ [apiTypes.API_RESPONSE_SOURCE_NAME]: 'lorem'
+ }
+ ]
+ })
+ });
- expect(component.render()).toMatchSnapshot('pending');
+ expect(component).toMatchSnapshot('fulfilled');
});
- it('should render a non-connected component with empty state', () => {
- const props = {};
-
- const component = mount(
);
+ it('should return an empty state when there are no sources', async () => {
+ const props = {
+ useGetSources: () => ({
+ fulfilled: true,
+ data: []
+ })
+ };
- expect(component.find('button').length).toEqual(1);
+ const component = await shallowHookComponent(
);
expect(component.render()).toMatchSnapshot('empty state');
});
});
diff --git a/src/components/sources/__tests__/sourcesContext.test.js b/src/components/sources/__tests__/sourcesContext.test.js
new file mode 100644
index 00000000..cbe6d6c9
--- /dev/null
+++ b/src/components/sources/__tests__/sourcesContext.test.js
@@ -0,0 +1,100 @@
+import { context, useGetSources, useOnDelete, usePoll } from '../sourcesContext';
+import { apiTypes } from '../../../constants/apiConstants';
+import { reduxTypes } from '../../../redux';
+
+describe('SourcesContext', () => {
+ it('should return specific properties', () => {
+ expect(context).toMatchSnapshot('specific properties');
+ });
+
+ it('should handle deleting a source with a confirmation', async () => {
+ const mockDispatch = jest.fn();
+ const mockSource = {
+ [apiTypes.API_RESPONSE_SOURCE_NAME]: 'lorem ipsum name',
+ [apiTypes.API_RESPONSE_SOURCE_ID]: 'dolor sit id'
+ };
+
+ // call confirmation
+ const { result: onDeleteConfirmation } = shallowHook(() => useOnDelete({ useDispatch: () => mockDispatch }));
+ onDeleteConfirmation(mockSource);
+
+ // delete results
+ await mountHook(() =>
+ useOnDelete({
+ useDispatch: () => mockDispatch,
+ useSelector: () => mockSource,
+ useSelectorsResponse: () => ({ fulfilled: true })
+ })
+ );
+
+ expect(mockDispatch.mock.calls).toMatchSnapshot('dispatch delete a source, confirmation');
+ mockDispatch.mockClear();
+ });
+
+ it('should attempt to poll sources', async () => {
+ const mockUseTimeout = jest.fn();
+ const options = {
+ pollInterval: 0,
+ useSelector: () => [{ connection: { status: 'pending' } }],
+ useTimeout: (callback, interval) => {
+ mockUseTimeout({
+ callback: callback(),
+ interval
+ });
+ return {};
+ }
+ };
+
+ await shallowHook(() => usePoll(options));
+ await shallowHook(() =>
+ usePoll({
+ ...options,
+ useSelector: () => []
+ })
+ );
+
+ expect(mockUseTimeout.mock.calls).toMatchSnapshot('timeout');
+ });
+
+ it('should apply a hook for retreiving data from multiple selectors', () => {
+ const { result: errorResponse } = shallowHook(() =>
+ useGetSources({
+ useSelectorsResponse: () => ({ error: true, message: 'Lorem ipsum' })
+ })
+ );
+
+ const { result: pendingResponse } = shallowHook(() =>
+ useGetSources({
+ useSelectorsResponse: () => ({ pending: true })
+ })
+ );
+
+ const { result: fulfilledResponse } = shallowHook(() =>
+ useGetSources({
+ useSelectorsResponse: () => ({ fulfilled: true, data: { view: { results: ['dolor', 'sit'] } } })
+ })
+ );
+
+ const { result: mockStoreSuccessResponse } = shallowHook(() => useGetSources(), {
+ state: {
+ viewOptions: {
+ [reduxTypes.view.SOURCES_VIEW]: {}
+ },
+ sources: {
+ expanded: {},
+ selected: {},
+ view: {
+ fulfilled: true,
+ data: {
+ results: ['lorem', 'ipsum']
+ }
+ }
+ }
+ }
+ });
+
+ expect({ errorResponse, fulfilledResponse, pendingResponse, mockStoreSuccessResponse }).toMatchSnapshot(
+ 'responses'
+ );
+ });
+});
diff --git a/src/components/sources/__tests__/sourcesEmptyState.test.js b/src/components/sources/__tests__/sourcesEmptyState.test.js
index 882365f6..ac9d6ba6 100644
--- a/src/components/sources/__tests__/sourcesEmptyState.test.js
+++ b/src/components/sources/__tests__/sourcesEmptyState.test.js
@@ -1,6 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
-import { EmptyState } from 'patternfly-react';
+import { Title } from '@patternfly/react-core';
import SourcesEmptyState from '../sourcesEmptyState';
describe('SourcesEmptyState Component', () => {
@@ -17,6 +17,6 @@ describe('SourcesEmptyState Component', () => {
};
const component = mount(
);
- expect(component.find(EmptyState.Title)).toMatchSnapshot('application name');
+ expect(component.find(Title)).toMatchSnapshot('application name');
});
});
diff --git a/src/components/sources/__tests__/sourcesTableCells.test.js b/src/components/sources/__tests__/sourcesTableCells.test.js
new file mode 100644
index 00000000..e96c4f87
--- /dev/null
+++ b/src/components/sources/__tests__/sourcesTableCells.test.js
@@ -0,0 +1,11 @@
+import { sourcesTableCells } from '../sourcesTableCells';
+
+describe('SourcesTableCells', () => {
+ it('should export specific function components', () => {
+ expect(sourcesTableCells).toMatchSnapshot('function components');
+ });
+
+ it('should return consistent cell results', () => {
+ Object.entries(sourcesTableCells).forEach(([key, value]) => expect(value()).toMatchSnapshot(`basic ${key} cell`));
+ });
+});
diff --git a/src/components/sources/sourceCredentialsList.js b/src/components/sources/sourceCredentialsList.js
deleted file mode 100644
index 8367da4f..00000000
--- a/src/components/sources/sourceCredentialsList.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Grid, Icon } from 'patternfly-react';
-import { apiTypes } from '../../constants/apiConstants';
-
-const SourceCredentialsList = ({ source }) => {
- const credentials =
- (source[apiTypes.API_RESPONSE_SOURCE_CREDENTIALS] && [...source[apiTypes.API_RESPONSE_SOURCE_CREDENTIALS]]) || [];
-
- credentials.sort((item1, item2) =>
- item1[apiTypes.API_RESPONSE_SOURCE_CREDENTIALS_NAME].localeCompare(
- item2[apiTypes.API_RESPONSE_SOURCE_CREDENTIALS_NAME]
- )
- );
-
- return (
-
- {credentials.map(credential => (
-
-
-
-
- {credential[apiTypes.API_RESPONSE_SOURCE_CREDENTIALS_NAME]}
-
-
-
- ))}
-
- );
-};
-
-SourceCredentialsList.propTypes = {
- source: PropTypes.shape({
- credentials: PropTypes.array
- }).isRequired
-};
-
-export { SourceCredentialsList as default, SourceCredentialsList };
diff --git a/src/components/sources/sourceListItem.js b/src/components/sources/sourceListItem.js
deleted file mode 100644
index f2fee6f9..00000000
--- a/src/components/sources/sourceListItem.js
+++ /dev/null
@@ -1,443 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import cx from 'classnames';
-import { Button, Checkbox, Grid, Icon, ListView } from 'patternfly-react';
-import _get from 'lodash/get';
-import _size from 'lodash/size';
-import { connect, reduxActions, reduxTypes, store } from '../../redux';
-import { helpers } from '../../common/helpers';
-import { dictionary } from '../../constants/dictionaryConstants';
-import SourceCredentialsList from './sourceCredentialsList';
-import ScanHostList from '../scanHostList/scanHostList';
-import ToolTip from '../tooltip/tooltip';
-import ListStatusItem from '../listStatusItem/listStatusItem';
-import Poll from '../poll/poll';
-import { apiTypes } from '../../constants/apiConstants';
-
-class SourceListItem extends React.Component {
- state = {
- expandType: null
- };
-
- onRefresh = () => {
- store.dispatch({
- type: reduxTypes.sources.UPDATE_SOURCES
- });
- };
-
- onItemSelectChange = event => {
- const { checked } = event.target;
- const { item } = this.props;
-
- store.dispatch({
- type: checked ? reduxTypes.view.SELECT_ITEM : reduxTypes.view.DESELECT_ITEM,
- viewType: reduxTypes.view.SOURCES_VIEW,
- item
- });
- };
-
- onToggleExpand = toggleExpandType => {
- const { expandType } = this.state;
- const toggle = expandType === toggleExpandType ? null : toggleExpandType;
-
- this.setState({ expandType: toggle });
- };
-
- onCloseExpand = () => {
- this.setState({ expandType: null });
- };
-
- onDelete = source => {
- const { deleteSource } = this.props;
-
- const onConfirm = () => {
- store.dispatch({
- type: reduxTypes.confirmationModal.CONFIRMATION_MODAL_HIDE
- });
-
- deleteSource(source[apiTypes.API_RESPONSE_SOURCE_ID]).then(
- () => {
- store.dispatch({
- type: reduxTypes.toastNotifications.TOAST_ADD,
- alertType: 'success',
- message: (
-
- Deleted source {source[apiTypes.API_RESPONSE_SOURCE_NAME]} .
-
- )
- });
-
- store.dispatch({
- type: reduxTypes.view.DESELECT_ITEM,
- viewType: reduxTypes.view.SOURCES_VIEW,
- item: source
- });
-
- store.dispatch({
- type: reduxTypes.sources.UPDATE_SOURCES
- });
- },
- error => {
- store.dispatch({
- type: reduxTypes.toastNotifications.TOAST_ADD,
- alertType: 'error',
- header: 'Error',
- message: helpers.getMessageFromResults(error).message
- });
- }
- );
- };
-
- store.dispatch({
- type: reduxTypes.confirmationModal.CONFIRMATION_MODAL_SHOW,
- title: 'Delete Source',
- heading: (
-
- Are you sure you want to delete the source {source[apiTypes.API_RESPONSE_SOURCE_NAME]} ?
-
- ),
- confirmButtonText: 'Delete',
- onConfirm
- });
- };
-
- onEdit = source => {
- store.dispatch({
- type: reduxTypes.sources.EDIT_SOURCE_SHOW,
- source
- });
- };
-
- onScan = source => {
- store.dispatch({
- type: reduxTypes.scans.EDIT_SCAN_SHOW,
- sources: [source]
- });
- };
-
- isSelected() {
- const { item, selectedSources } = this.props;
-
- return (
- selectedSources.find(nextSelected => nextSelected[apiTypes.API_RESPONSE_SOURCE_ID] === item.id) !== undefined
- );
- }
-
- renderSourceType() {
- const { item } = this.props;
- const typeIcon = helpers.sourceTypeIcon(item.source_type);
-
- return (
-
-
-
- );
- }
-
- renderActions() {
- const { item } = this.props;
-
- return (
-
-
- this.onEdit(item)} bsStyle="link">
-
-
-
-
- this.onDelete(item)} bsStyle="link">
-
-
-
- this.onScan(item)}>Scan
-
- );
- }
-
- renderStatusItems() {
- const { expandType } = this.state;
- const { item } = this.props;
-
- const credentialCount = _size(_get(item, 'credentials', []));
- let okHostCount = _get(item, 'connection.source_systems_scanned', 0);
- let failedHostCount = _get(item, 'connection.source_systems_failed', 0);
- const unreachableHostCount = _get(item, 'connection.source_systems_unreachable', 0);
-
- if (helpers.DEV_MODE) {
- okHostCount = helpers.devModeNormalizeCount(okHostCount);
- failedHostCount = helpers.devModeNormalizeCount(failedHostCount);
- }
-
- return [
-
,
-
,
-
,
-
- ];
- }
-
- static renderHostRow(host) {
- const iconInfo = helpers.scanStatusIcon(host.status);
-
- return (
-
-
-
-
- {host.name}
-
-
- {host.status === 'success' && (
-
-
-
- {host.credentialName}
-
-
- )}
-
- );
- }
-
- renderExpansionContents() {
- const { expandType } = this.state;
- const { item, lastRefresh } = this.props;
-
- switch (expandType) {
- case 'okHosts':
- return (
-
- {({ host }) => SourceListItem.renderHostRow(host)}
-
- );
- case 'failedHosts':
- return (
-
- {({ host }) => SourceListItem.renderHostRow(host)}
-
- );
- case 'unreachableHosts':
- return (
-
- {({ host }) => SourceListItem.renderHostRow(host)}
-
- );
- case 'credentials':
- return
;
- default:
- return null;
- }
- }
-
- renderDescription() {
- const { item } = this.props;
-
- const itemHostsPopover = (
-
- {item.hosts && item.hosts.length > 1 && (
-
- {item.hosts.map(host => (
- {host}
- ))}
-
- )}
- {item.hosts && item.hosts.length === 1 &&
{item.hosts[0]}
}
-
- );
-
- let itemDescription;
-
- if (_size(item.hosts)) {
- if (item.source_type === 'network') {
- itemDescription = (
-
-
-
- Network Range
-
-
-
- );
- } else {
- itemDescription =
{item.hosts[0]} ;
- }
- }
-
- return (
-
-
- {item.name}
- {itemDescription}
-
- {this.renderScanStatus()}
-
- );
- }
-
- renderScanStatus() {
- const { item } = this.props;
-
- const scan = _get(item, 'connection');
- let scanDescription = '';
- let scanTime = _get(scan, 'end_time');
- let icon = null;
-
- switch (_get(scan, 'status')) {
- case 'completed':
- scanDescription = 'Last Connected';
- icon =
;
- break;
- case 'failed':
- scanDescription = 'Connection Failed';
- icon =
;
- break;
- case 'canceled':
- scanDescription = 'Connection Canceled';
- icon =
;
- break;
- case 'created':
- case 'pending':
- case 'running':
- scanTime = _get(scan, 'start_time');
- scanDescription = 'Connection in Progress';
- icon =
;
- break;
- case 'paused':
- scanDescription = 'Connection Paused';
- icon =
;
- break;
- default:
- return null;
- }
-
- return (
-
- {icon}
-
-
{scanDescription}
-
{helpers.getTimeDisplayHowLongAgo(scanTime)}
-
-
- );
- }
-
- render() {
- const { expandType } = this.state;
- const { item, pollInterval } = this.props;
- const selected = this.isSelected();
- const sourceStatus = _get(item, ['connection', 'status']);
-
- return (
-
- }
- actions={this.renderActions()}
- leftContent={this.renderSourceType()}
- description={this.renderDescription()}
- additionalInfo={this.renderStatusItems()}
- compoundExpand
- compoundExpanded={expandType !== null}
- onCloseCompoundExpand={this.onCloseExpand}
- >
- {this.renderExpansionContents()}
-
-
- );
- }
-}
-
-SourceListItem.propTypes = {
- deleteSource: PropTypes.func,
- item: PropTypes.object.isRequired,
- lastRefresh: PropTypes.number,
- pollInterval: PropTypes.number,
- selectedSources: PropTypes.array
-};
-
-SourceListItem.defaultProps = {
- deleteSource: helpers.noop,
- lastRefresh: 0,
- pollInterval: 120000,
- selectedSources: []
-};
-
-const mapDispatchToProps = dispatch => ({
- deleteSource: id => dispatch(reduxActions.sources.deleteSource(id))
-});
-
-const mapStateToProps = state => ({ selectedSources: state.viewOptions[reduxTypes.view.SOURCES_VIEW].selectedItems });
-
-const ConnectedSourceListItem = connect(mapStateToProps, mapDispatchToProps)(SourceListItem);
-
-export { ConnectedSourceListItem as default, ConnectedSourceListItem, SourceListItem };
diff --git a/src/components/sources/sources.js b/src/components/sources/sources.js
index 83190629..b01d3be3 100644
--- a/src/components/sources/sources.js
+++ b/src/components/sources/sources.js
@@ -1,199 +1,248 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Alert, Button, EmptyState, ListView, Modal, Spinner } from 'patternfly-react';
-import _isEqual from 'lodash/isEqual';
-import _size from 'lodash/size';
-import { connect, reduxActions, reduxTypes, store } from '../../redux';
-import helpers from '../../common/helpers';
+import { Alert, AlertVariant, Button, ButtonVariant, EmptyState, Spinner } from '@patternfly/react-core';
+import { IconSize } from '@patternfly/react-icons';
+import { Modal, ModalVariant } from '../modal/modal';
+import { reduxTypes, storeHooks } from '../../redux';
import ViewToolbar from '../viewToolbar/viewToolbar';
import ViewPaginationRow from '../viewPaginationRow/viewPaginationRow';
import SourcesEmptyState from './sourcesEmptyState';
-import SourceListItem from './sourceListItem';
import { SourceFilterFields, SourceSortFields } from './sourceConstants';
-import { apiTypes } from '../../constants/apiConstants';
-
-class Sources extends React.Component {
- componentDidMount() {
- const { getSources, viewOptions } = this.props;
-
- getSources(helpers.createViewQueryObject(viewOptions));
- }
-
- componentDidUpdate(prevProps) {
- const { getSources, updateSources, viewOptions } = this.props;
-
- const prevQuery = helpers.createViewQueryObject(prevProps.viewOptions);
- const nextQuery = helpers.createViewQueryObject(viewOptions);
-
- if (updateSources || !_isEqual(prevQuery, nextQuery)) {
- getSources(nextQuery);
- }
- }
-
- onShowAddSourceWizard = () => {
- store.dispatch({
- type: reduxTypes.sources.CREATE_SOURCE_SHOW
- });
- };
-
- onScanSources = () => {
- const { viewOptions } = this.props;
-
- store.dispatch({
+import { translate } from '../i18n/i18n';
+import { Table } from '../table/table';
+import { sourcesTableCells } from './sourcesTableCells';
+import {
+ useGetSources,
+ useOnDelete,
+ useOnEdit,
+ useOnExpand,
+ useOnRefresh,
+ useOnScan,
+ useOnSelect
+} from './sourcesContext';
+import { useOnShowAddSourceWizard } from '../addSourceWizard/addSourceWizardContext';
+
+const VIEW_ID = 'sources';
+
+/**
+ * A sources view.
+ *
+ * @param {object} props
+ * @param {Function} props.t
+ * @param {Function} props.useGetSources
+ * @param {Function} props.useOnDelete
+ * @param {Function} props.useOnEdit
+ * @param {Function} props.useOnExpand
+ * @param {Function} props.useOnRefresh
+ * @param {Function} props.useOnScan
+ * @param {Function} props.useOnSelect
+ * @param {Function} props.useOnShowAddSourceWizard
+ * @param {Function} props.useDispatch
+ * @param {Function} props.useSelectors
+ * @param {string} props.viewId
+ * @returns {React.ReactNode}
+ */
+const Sources = ({
+ t,
+ useGetSources: useAliasGetSources,
+ useOnDelete: useAliasOnDelete,
+ useOnEdit: useAliasOnEdit,
+ useOnExpand: useAliasOnExpand,
+ useOnRefresh: useAliasOnRefresh,
+ useOnScan: useAliasOnScan,
+ useOnSelect: useAliasOnSelect,
+ useOnShowAddSourceWizard: useAliasOnShowAddSourceWizard,
+ useDispatch: useAliasDispatch,
+ useSelectors: useAliasSelectors,
+ viewId
+}) => {
+ const dispatch = useAliasDispatch();
+ const onDelete = useAliasOnDelete();
+ const onEdit = useAliasOnEdit();
+ const onExpand = useAliasOnExpand();
+ const onRefresh = useAliasOnRefresh();
+ const onScan = useAliasOnScan();
+ const onSelect = useAliasOnSelect();
+ const onShowAddSourceWizard = useAliasOnShowAddSourceWizard();
+ const { pending, error, errorMessage, date, data, selectedRows = {}, expandedRows = {} } = useAliasGetSources();
+ const [viewOptions = {}] = useAliasSelectors([
+ ({ viewOptions: stateViewOptions }) => stateViewOptions[reduxTypes.view.SOURCES_VIEW]
+ ]);
+ const filtersOrSourcesActive = viewOptions?.activeFilters?.length > 0 || data?.length > 0 || false;
+
+ // ToDo: review onScanSources, renderToolbarActions being standalone with upcoming toolbar updates
+ /**
+ * Toolbar actions onScanSources
+ *
+ * @event onScanSources
+ */
+ const onScanSources = () => {
+ dispatch({
type: reduxTypes.scans.EDIT_SCAN_SHOW,
- sources: viewOptions.selectedItems
- });
- };
-
- onRefresh = () => {
- store.dispatch({
- type: reduxTypes.sources.UPDATE_SOURCES
+ sources: Object.values(selectedRows).filter(val => val !== null)
});
};
- onClearFilters = () => {
- store.dispatch({
- type: reduxTypes.viewToolbar.CLEAR_FILTERS,
- viewType: reduxTypes.view.SOURCES_VIEW
- });
- };
-
- renderSourceActions() {
- const { viewOptions } = this.props;
-
+ /**
+ * Return toolbar actions.
+ *
+ * @returns {React.ReactNode}
+ */
+ const renderToolbarActions = () => (
+
+ {t('table.label', { context: 'add' })} {' '}
+ val !== null).length === 0}
+ onClick={onScanSources}
+ >
+ {t('table.label', { context: 'scan' })}
+
+
+ );
+
+ if (pending) {
return (
-
-
- Add
-
-
- Scan
-
-
+
+
+ {t('view.loading', { context: viewId })}
+
);
}
- renderPendingMessage() {
- const { pending } = this.props;
-
- if (pending) {
- return (
-
-
-
- Loading...
-
-
- );
- }
-
- return null;
- }
-
- renderSourcesList(sources) {
- const { lastRefresh } = this.props;
-
- if (sources.length) {
- return (
-
- {sources.map(source => (
-
- ))}
-
- );
- }
-
+ if (error) {
return (
-
- No Results Match the Filter Criteria
- The active filters are hiding all items.
-
-
- Clear Filters
-
-
+
+
+ {t('view.error-message', { context: [viewId], message: errorMessage })}
+
);
}
- render() {
- const { error, errorMessage, lastRefresh, pending, sources, viewOptions } = this.props;
-
- if (error) {
- return (
-
-
- Error retrieving sources: {errorMessage}
-
- {this.renderPendingMessage()}
-
- );
- }
-
- if (pending && !sources.length) {
- return {this.renderPendingMessage()}
;
- }
-
- if (sources.length || _size(viewOptions.activeFilters)) {
- return (
-
+ return (
+
+ {filtersOrSourcesActive && (
+
onRefresh()}
+ lastRefresh={new Date(date).getTime()}
+ actions={renderToolbarActions()}
itemsType="Source"
itemsTypePlural="Sources"
- selectedCount={viewOptions.selectedItems.length}
+ selectedCount={viewOptions.selectedItems?.length}
{...viewOptions}
/>
- {this.renderSourcesList(sources)}
- {this.renderPendingMessage()}
-
- );
- }
-
- return
;
- }
-}
+
+ )}
+
+
({
+ isSelected: (selectedRows?.[item.id] && true) || false,
+ source: item,
+ cells: [
+ {
+ content: sourcesTableCells.description(item),
+ dataLabel: t('table.header', { context: ['description'] })
+ },
+ {
+ content: sourcesTableCells.scanStatus(item, { viewId }),
+ width: 20,
+ dataLabel: t('table.header', { context: ['scan'] })
+ },
+ {
+ ...sourcesTableCells.credentialsCellContent(item),
+ isExpanded: expandedRows?.[item.id] === 2,
+ width: 8,
+ dataLabel: t('table.header', { context: ['credentials'] })
+ },
+ {
+ ...sourcesTableCells.okHostsCellContent(item, { viewId }),
+ isExpanded: expandedRows?.[item.id] === 3,
+ width: 8,
+ dataLabel: t('table.header', { context: ['success', viewId] })
+ },
+ {
+ ...sourcesTableCells.failedHostsCellContent(item, { viewId }),
+ isExpanded: expandedRows?.[item.id] === 4,
+ width: 8,
+ dataLabel: t('table.header', { context: ['failed', viewId] })
+ },
+ {
+ ...sourcesTableCells.unreachableHostsCellContent(item, { viewId }),
+ isExpanded: expandedRows?.[item.id] === 5,
+ width: 8,
+ dataLabel: t('table.header', { context: ['unreachable', viewId] })
+ },
+ {
+ content: sourcesTableCells.actionsCell({
+ isFirst: index === 0,
+ isLast: index === data.length - 1,
+ item,
+ onDelete: () => onDelete(item),
+ onEdit: () => onEdit(item),
+ onScan: () => onScan(item)
+ }),
+ isActionCell: true
+ }
+ ]
+ }))}
+ >
+
+
+
+
+ );
+};
+/**
+ * Prop types
+ *
+ * @type {{useOnEdit: Function, useOnSelect: Function, viewId: string, t: Function, useOnRefresh: Function, useOnScan: Function,
+ * useDispatch: Function, useOnDelete: Function, useOnExpand: Function, useGetSources: Function, useSelectors: Function,
+ * useOnShowAddSourceWizard: Function}}
+ */
Sources.propTypes = {
- error: PropTypes.bool,
- errorMessage: PropTypes.string,
- getSources: PropTypes.func,
- lastRefresh: PropTypes.number,
- pending: PropTypes.bool,
- sources: PropTypes.array,
- updateSources: PropTypes.bool,
- viewOptions: PropTypes.object
+ t: PropTypes.func,
+ useDispatch: PropTypes.func,
+ useGetSources: PropTypes.func,
+ useOnDelete: PropTypes.func,
+ useOnEdit: PropTypes.func,
+ useOnExpand: PropTypes.func,
+ useOnRefresh: PropTypes.func,
+ useOnScan: 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, useOnScan: Function,
+ * useDispatch: Function, useOnDelete: Function, useOnExpand: Function, useGetSources: Function, useSelectors: Function,
+ * useOnShowAddSourceWizard: Function}}
+ */
Sources.defaultProps = {
- error: false,
- errorMessage: null,
- getSources: helpers.noop,
- lastRefresh: 0,
- pending: false,
- sources: [],
- updateSources: false,
- viewOptions: {}
+ t: translate,
+ useDispatch: storeHooks.reactRedux.useDispatch,
+ useGetSources,
+ useOnDelete,
+ useOnEdit,
+ useOnExpand,
+ useOnRefresh,
+ useOnScan,
+ useOnSelect,
+ useOnShowAddSourceWizard,
+ useSelectors: storeHooks.reactRedux.useSelectors,
+ viewId: VIEW_ID
};
-const mapDispatchToProps = dispatch => ({
- getSources: queryObj => dispatch(reduxActions.sources.getSources(queryObj))
-});
-
-const mapStateToProps = state => ({
- ...state.sources.view,
- viewOptions: state.viewOptions[reduxTypes.view.SOURCES_VIEW]
-});
-
-const ConnectedSources = connect(mapStateToProps, mapDispatchToProps)(Sources);
-
-export { ConnectedSources as default, ConnectedSources, Sources };
+export { Sources as default, Sources, VIEW_ID };
diff --git a/src/components/sources/sourcesContext.js b/src/components/sources/sourcesContext.js
new file mode 100644
index 00000000..2cc28f41
--- /dev/null
+++ b/src/components/sources/sourcesContext.js
@@ -0,0 +1,307 @@
+import React, { useEffect } from 'react';
+import { useShallowCompareEffect } from 'react-use';
+import { reduxActions, reduxTypes, storeHooks } from '../../redux';
+import { useTimeout } from '../../hooks';
+import { apiTypes } from '../../constants/apiConstants';
+import { helpers } from '../../common';
+import { translate } from '../i18n/i18n';
+
+/**
+ * On Delete confirmation, and action.
+ *
+ * @param {object} options
+ * @param {Function} options.deleteSource
+ * @param {Function} options.t
+ * @param {Function} options.useDispatch
+ * @param {Function} options.useSelector
+ * @param {Function} options.useSelectorsResponse
+ * @returns {Function}
+ */
+const useOnDelete = ({
+ deleteSource = reduxActions.sources.deleteSource,
+ t = translate,
+ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch,
+ useSelector: useAliasSelector = storeHooks.reactRedux.useSelector,
+ useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse
+} = {}) => {
+ const dispatch = useAliasDispatch();
+ const sourceToDelete = useAliasSelector(({ sources }) => sources?.confirmDelete?.source, {});
+ const { error, fulfilled, message } = useAliasSelectorsResponse(({ sources }) => sources?.deleted);
+ const { [apiTypes.API_RESPONSE_SOURCE_ID]: sourceId, [apiTypes.API_RESPONSE_SOURCE_NAME]: sourceName } =
+ sourceToDelete;
+
+ useEffect(() => {
+ if (sourceId) {
+ dispatch([
+ {
+ type: reduxTypes.confirmationModal.CONFIRMATION_MODAL_HIDE
+ }
+ ]);
+
+ deleteSource(sourceId)(dispatch);
+ }
+ }, [sourceId, deleteSource, dispatch]);
+
+ useShallowCompareEffect(() => {
+ if (fulfilled) {
+ dispatch([
+ {
+ type: reduxTypes.toastNotifications.TOAST_ADD,
+ alertType: 'success',
+ header: t('toast-notifications.title', {
+ context: ['deleted-source']
+ }),
+ message: t('toast-notifications.description', {
+ context: ['deleted-source'],
+ name: sourceName
+ })
+ },
+ {
+ type: reduxTypes.sources.RESET_DELETE_SOURCE
+ },
+ {
+ type: reduxTypes.view.DESELECT_ITEM,
+ viewType: reduxTypes.view.SOURCES_VIEW,
+ item: sourceToDelete
+ },
+ {
+ type: reduxTypes.sources.UPDATE_SOURCES
+ }
+ ]);
+ }
+
+ if (error) {
+ dispatch({
+ type: reduxTypes.toastNotifications.TOAST_ADD,
+ alertType: 'danger',
+ header: t('toast-notifications.title', {
+ context: ['error']
+ }),
+ message
+ });
+ }
+ }, [error, fulfilled, message, dispatch, sourceName, sourceToDelete]);
+
+ return source => {
+ dispatch({
+ type: reduxTypes.confirmationModal.CONFIRMATION_MODAL_SHOW,
+ title: t('form-dialog.confirmation', {
+ context: ['title', 'delete-source']
+ }),
+ heading: t(
+ 'form-dialog.confirmation',
+ {
+ context: ['heading', 'delete-source'],
+ name: source[apiTypes.API_RESPONSE_SOURCE_NAME]
+ },
+ [ ]
+ ),
+ confirmButtonText: t('form-dialog.label', {
+ context: ['delete']
+ }),
+ onConfirm: () =>
+ dispatch({
+ type: reduxTypes.sources.CONFIRM_DELETE_SOURCE,
+ source
+ })
+ });
+ };
+};
+
+/**
+ * On edit a source
+ *
+ * @param {object} options
+ * @param {Function} options.useDispatch
+ * @returns {Function}
+ */
+const useOnEdit = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => {
+ const dispatch = useAliasDispatch();
+
+ return source => {
+ dispatch({
+ type: reduxTypes.sources.EDIT_SOURCE_SHOW,
+ source
+ });
+ };
+};
+
+/**
+ * On expand a source 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: sourceData }) => {
+ dispatch({
+ type: isExpanded ? reduxTypes.sources.EXPANDED_SOURCE : reduxTypes.sources.NOT_EXPANDED_SOURCE,
+ viewType: reduxTypes.view.SOURCES_VIEW,
+ source: sourceData.source,
+ cellIndex
+ });
+ };
+};
+
+/**
+ * On refresh sources view.
+ *
+ * @param {object} options
+ * @param {Function} options.useDispatch
+ * @returns {Function}
+ */
+const useOnRefresh = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => {
+ const dispatch = useAliasDispatch();
+
+ return () => {
+ dispatch({
+ type: reduxTypes.sources.UPDATE_SOURCES
+ });
+ };
+};
+
+/**
+ * On scan a source
+ *
+ * @param {object} options
+ * @param {Function} options.useDispatch
+ * @returns {Function}
+ */
+const useOnScan = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => {
+ const dispatch = useAliasDispatch();
+
+ return source => {
+ dispatch({
+ type: reduxTypes.scans.EDIT_SCAN_SHOW,
+ sources: [source]
+ });
+ };
+};
+
+/**
+ * On select a source row.
+ *
+ * @param {object} options
+ * @param {Function} options.useDispatch
+ * @returns {Function}
+ */
+const useOnSelect = ({ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch } = {}) => {
+ const dispatch = useAliasDispatch();
+
+ return ({ isSelected, data: sourceData }) => {
+ dispatch({
+ type: isSelected ? reduxTypes.sources.SELECT_SOURCE : reduxTypes.sources.DESELECT_SOURCE,
+ viewType: reduxTypes.view.SOURCES_VIEW,
+ source: sourceData.source
+ });
+ };
+};
+
+/**
+ * Poll sources data for pending results.
+ *
+ * @param {object} options
+ * @param {number} options.pollInterval
+ * @param {Function} options.useSelector
+ * @param {Function} options.useTimeout
+ * @returns {Function}
+ */
+const usePoll = ({
+ pollInterval = helpers.POLL_INTERVAL,
+ useSelector: useAliasSelector = storeHooks.reactRedux.useSelector,
+ useTimeout: useAliasTimeout = useTimeout
+} = {}) => {
+ const updatedSources = useAliasSelector(({ sources }) => sources?.view?.data?.results, []);
+ const { update } = useAliasTimeout(() => {
+ const filteredSources = updatedSources.filter(
+ ({ connection }) =>
+ connection?.status === 'created' || connection?.status === 'pending' || connection?.status === 'running'
+ );
+
+ return filteredSources.length > 0;
+ }, pollInterval);
+
+ return update;
+};
+
+/**
+ * Get sources
+ *
+ * @param {object} options
+ * @param {Function} options.getSources
+ * @param {Function} options.useDispatch
+ * @param {Function} options.usePoll
+ * @param {Function} options.useSelectors
+ * @param {Function} options.useSelectorsResponse
+ * @returns {{date: *, sources: *[], expandedSources: *, pending: boolean, errorMessage: null, fulfilled: boolean, error: boolean, selectedSources: *}}
+ */
+const useGetSources = ({
+ getSources = reduxActions.sources.getSources,
+ useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch,
+ usePoll: useAliasPoll = usePoll,
+ useSelectors: useAliasSelectors = storeHooks.reactRedux.useSelectors,
+ useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse
+} = {}) => {
+ const dispatch = useAliasDispatch();
+ const pollUpdate = useAliasPoll();
+ const [refreshUpdate, selectedRows, expandedRows, viewOptions] = useAliasSelectors([
+ ({ sources }) => sources?.update,
+ ({ sources }) => sources?.selected,
+ ({ sources }) => sources?.expanded,
+ ({ viewOptions: stateViewOptions }) => stateViewOptions?.[reduxTypes.view.SOURCES_VIEW]
+ ]);
+ const {
+ data: responseData,
+ error,
+ fulfilled,
+ message: errorMessage,
+ pending,
+ responses = {}
+ } = useAliasSelectorsResponse({ id: 'view', selector: ({ sources }) => sources?.view });
+
+ const [{ date } = {}] = responses?.list || [];
+ const { results: data = [] } = responseData?.view || {};
+ const query = helpers.createViewQueryObject(viewOptions);
+
+ useShallowCompareEffect(() => {
+ getSources(query)(dispatch);
+ }, [dispatch, getSources, pollUpdate, query, refreshUpdate]);
+
+ return {
+ pending,
+ error,
+ errorMessage,
+ fulfilled,
+ data,
+ date,
+ selectedRows,
+ expandedRows
+ };
+};
+
+const context = {
+ useGetSources,
+ useOnDelete,
+ useOnEdit,
+ useOnExpand,
+ useOnRefresh,
+ useOnScan,
+ useOnSelect,
+ usePoll
+};
+
+export {
+ context as default,
+ context,
+ useGetSources,
+ useOnDelete,
+ useOnEdit,
+ useOnExpand,
+ useOnRefresh,
+ useOnScan,
+ useOnSelect,
+ usePoll
+};
diff --git a/src/components/sources/sourcesEmptyState.js b/src/components/sources/sourcesEmptyState.js
index 0bd2b298..c24edb50 100644
--- a/src/components/sources/sourcesEmptyState.js
+++ b/src/components/sources/sourcesEmptyState.js
@@ -1,37 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Button, EmptyState, Grid, Row } from 'patternfly-react';
+import {
+ Button,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateIcon,
+ EmptyStatePrimary,
+ EmptyStateVariant,
+ Title
+} from '@patternfly/react-core';
+import { AddCircleOIcon } from '@patternfly/react-icons';
import helpers from '../../common/helpers';
+import { translate } from '../i18n/i18n';
-const SourcesEmptyState = ({ onAddSource, uiShortName }) => (
-
-
-
-
- Welcome to {uiShortName}
-
- Begin by adding one or more sources. A source contains a collection of network information,
- including systems management solution information, IP addresses, or host names, in addition to
- SSH ports and credentials.
-
-
-
- Add Source
-
-
-
-
-
+/**
+ * Return a sources empty state.
+ *
+ * @param {object} props
+ * @param {Function} props.onAddSource
+ * @param {Function} props.t
+ * @param {string} props.uiShortName
+ * @param {string} props.viewId
+ * @returns {React.ReactNode}
+ */
+const SourcesEmptyState = ({ onAddSource, t, uiShortName, viewId }) => (
+
+
+ {t('view.empty-state', { context: ['title'], name: uiShortName })}
+ {t('view.empty-state', { context: ['description', viewId] })}
+
+ {t('view.empty-state', { context: ['label', viewId] })}
+
+
);
+/**
+ * Prop types
+ *
+ * @type {{uiShortName: string, viewId: string, t: Function, onAddSource: Function}}
+ */
SourcesEmptyState.propTypes = {
onAddSource: PropTypes.func,
- uiShortName: PropTypes.string
+ uiShortName: PropTypes.string,
+ t: PropTypes.func,
+ viewId: PropTypes.string
};
+/**
+ * Default props
+ *
+ * @type {{uiShortName: string, viewId: null, t: translate, onAddSource: Function}}
+ */
SourcesEmptyState.defaultProps = {
onAddSource: helpers.noop,
- uiShortName: helpers.UI_SHORT_NAME
+ uiShortName: helpers.UI_SHORT_NAME,
+ t: translate,
+ viewId: null
};
export { SourcesEmptyState as default, SourcesEmptyState };
diff --git a/src/components/sources/sourcesTableCells.js b/src/components/sources/sourcesTableCells.js
new file mode 100644
index 00000000..25d7298f
--- /dev/null
+++ b/src/components/sources/sourcesTableCells.js
@@ -0,0 +1,392 @@
+import React from 'react';
+import {
+ Button,
+ ButtonVariant,
+ Grid,
+ GridItem,
+ List,
+ ListItem,
+ OverflowMenu,
+ OverflowMenuControl,
+ OverflowMenuContent,
+ OverflowMenuGroup,
+ OverflowMenuItem
+} from '@patternfly/react-core';
+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';
+import { helpers } from '../../common';
+import { DropdownSelect, SelectButtonVariant, SelectDirection, SelectPosition } from '../dropdownSelect/dropdownSelect';
+
+/**
+ * Source description and type icon
+ *
+ * @param {object} params
+ * @param {Array} params.hosts
+ * @param {string} params.name
+ * @param {string} params.source_type
+ * @param {object} options
+ * @param {Function} options.t
+ * @returns {React.ReactNode}
+ */
+const description = ({ hosts, name, source_type: sourceType } = {}, { t = translate } = {}) => {
+ const itemHostsPopover = (
+
+ {hosts?.length > 1 && (
+
+ {hosts.map(host => (
+ {host}
+ ))}
+
+ )}
+ {hosts?.length === 1 &&
{hosts[0]}
}
+
+ );
+
+ let itemDescription;
+
+ if (hosts?.length) {
+ if (sourceType === 'network') {
+ itemDescription = (
+
+
+ {t('table.label', { context: 'network-range' })}
+
+
+ );
+ } else {
+ [itemDescription] = hosts;
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {name}
+
+ {itemDescription}
+
+
+ );
+};
+
+/**
+ * Scan status, icon and description
+ *
+ * @param {object} params
+ * @param {object} params.connection
+ * @param {object} options
+ * @param {Function} options.t
+ * @param {string} options.viewId
+ * @returns {React.ReactNode|null}
+ */
+const scanStatus = ({ connection: scan = {} } = {}, { t = translate, viewId } = {}) => {
+ const { end_time: endTime, start_time: startTime, status } = scan;
+ const isPending = status === 'created' || status === 'pending' || status === 'running';
+ const scanTime = (isPending && startTime) || endTime;
+
+ return (
+
+
+
+
+
+ {t('table.label', { context: ['status', status, viewId] })}
+ {helpers.getTimeDisplayHowLongAgo(scanTime)}
+
+
+ );
+};
+
+/**
+ * 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 a consistent display row for expandable content.
+ *
+ * @param {object} params
+ * @param {object} params.connection
+ * @param {string} params.id
+ * @param {string} params.status
+ * @param {object} options
+ * @param {boolean} options.useConnectionResults
+ * @param {boolean} options.useInspectionResults
+ * @returns {React.ReactNode}
+ */
+const statusContent = (
+ { connection, id, status } = {},
+ { useConnectionResults = true, useInspectionResults = false } = {}
+) => (
+
+ {({ host }) => (
+
+
+ {host?.name}
+
+ {host?.status === 'success' && (
+
+ {host?.credentialName}
+
+ )}
+
+ )}
+
+);
+
+/**
+ * Generate credentials expandable content.
+ *
+ * @param {object} source
+ * @returns {React.ReactNode}
+ */
+const credentialsContent = ({ [apiTypes.API_RESPONSE_SOURCE_CREDENTIALS]: sourceCredentials } = {}) => {
+ const credentials = (sourceCredentials && [...sourceCredentials]) || [];
+
+ credentials.sort((item1, item2) =>
+ item1[apiTypes.API_RESPONSE_SOURCE_CREDENTIALS_NAME].localeCompare(
+ item2[apiTypes.API_RESPONSE_SOURCE_CREDENTIALS_NAME]
+ )
+ );
+
+ return (
+
+ {credentials?.map(credential => (
+ }
+ >
+ {credential[apiTypes.API_RESPONSE_SOURCE_CREDENTIALS_NAME]}
+
+ ))}
+
+ );
+};
+
+/**
+ * Credentials cell status, and expandable content
+ *
+ * @param {object} item
+ * @param {Array} item.credentials
+ * @returns {{content: React.ReactNode, status: React.ReactNode}}
+ */
+const credentialsCellContent = (item = {}) => {
+ const { credentials = [] } = item;
+ const count = credentials?.length;
+
+ return {
+ content: statusCell({ count, status: ContextIconVariant.idCard }),
+ expandedContent: (count && credentialsContent(item)) || undefined
+ };
+};
+
+/**
+ * Failed hosts cell and expandable content.
+ *
+ * @param {object} params
+ * @param {object} params.connection
+ * @param {string} params.id
+ * @param {object} options
+ * @param {string} options.viewId
+ * @returns {{cell: React.ReactNode, content: React.ReactNode}}
+ */
+const failedHostsCellContent = ({ connection, id } = {}, { viewId } = {}) => {
+ const count = Number.parseInt(connection?.source_systems_failed, 10);
+
+ return {
+ content: statusCell({ count, status: ContextIconVariant.failed, viewId }),
+ expandedContent: (count && statusContent({ connection, id, status: ContextIconVariant.failed })) || undefined
+ };
+};
+
+/**
+ * Ok hosts cell and expandable content.
+ *
+ * @param {object} params
+ * @param {object} params.connection
+ * @param {string} params.id
+ * @param {object} options
+ * @param {string} options.viewId
+ * @returns {{cell: React.ReactNode, content: React.ReactNode}}
+ */
+const okHostsCellContent = ({ connection, id } = {}, { viewId } = {}) => {
+ const count = Number.parseInt(connection?.source_systems_scanned, 10);
+
+ return {
+ content: statusCell({ count, status: ContextIconVariant.success, viewId }),
+ expandedContent: (count && statusContent({ connection, id, status: ContextIconVariant.success })) || undefined
+ };
+};
+
+/**
+ * Unreachable hosts cell and expandable content.
+ *
+ * @param {object} params
+ * @param {object} params.connection
+ * @param {string} params.id
+ * @param {object} options
+ * @param {string} options.viewId
+ * @returns {{cell: React.ReactNode, content: React.ReactNode}}
+ */
+const unreachableHostsCellContent = ({ connection, id } = {}, { viewId } = {}) => {
+ const count = Number.parseInt(connection?.source_systems_unreachable, 10);
+
+ return {
+ content: statusCell({ count, status: ContextIconVariant.unreachable, viewId }),
+ expandedContent: (count && statusContent({ connection, id, status: ContextIconVariant.unreachable })) || 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.onScan
+ * @param {Function} params.onDelete
+ * @param {Function} params.onEdit
+ * @param {Function} params.t
+ * @returns {React.ReactNode}
+ */
+const actionsCell = ({
+ isFirst = false,
+ isLast = false,
+ item = {},
+ onScan = helpers.noop,
+ onDelete = helpers.noop,
+ onEdit = helpers.noop,
+ t = translate
+} = {}) => {
+ const onSelect = ({ value }) => {
+ switch (value) {
+ case 'edit':
+ return onEdit(item);
+ case 'delete':
+ return onDelete(item);
+ case 'scan':
+ default:
+ return onScan(item);
+ }
+ };
+
+ return (
+
+
+
+
+
+ onEdit(item)}
+ aria-label={t('table.label', { context: 'edit' })}
+ variant={ButtonVariant.plain}
+ >
+
+
+
+
+
+
+ onDelete(item)}
+ aria-label={t('table.label', { context: 'delete' })}
+ variant={ButtonVariant.plain}
+ >
+
+
+
+
+
+ onScan(item)}>
+ {t('table.label', { context: 'scan' })}
+
+
+
+
+
+ }
+ options={[
+ { title: t('table.label', { context: 'edit' }), value: 'edit' },
+ { title: t('table.label', { context: 'delete' }), value: 'delete' },
+ { title: t('table.label', { context: 'Scan' }), value: 'scan' }
+ ]}
+ />
+
+
+ );
+};
+
+const sourcesTableCells = {
+ actionsCell,
+ credentialsCellContent,
+ description,
+ failedHostsCellContent,
+ okHostsCellContent,
+ scanStatus,
+ statusCell,
+ statusContent,
+ unreachableHostsCellContent
+};
+
+export {
+ sourcesTableCells as default,
+ sourcesTableCells,
+ actionsCell,
+ credentialsCellContent,
+ description,
+ failedHostsCellContent,
+ okHostsCellContent,
+ scanStatus,
+ statusCell,
+ statusContent,
+ unreachableHostsCellContent
+};
diff --git a/src/components/table/__tests__/__snapshots__/table.test.js.snap b/src/components/table/__tests__/__snapshots__/table.test.js.snap
new file mode 100644
index 00000000..5f941d3d
--- /dev/null
+++ b/src/components/table/__tests__/__snapshots__/table.test.js.snap
@@ -0,0 +1,775 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Table Component should allow expandable cell content: expand cell event 1`] = `
+Array [
+ Array [
+ Object {
+ "cellIndex": 0,
+ "data": Object {
+ "data": Object {
+ "hello": "world",
+ },
+ },
+ "isExpanded": true,
+ "rowIndex": 0,
+ "type": "compound",
+ },
+ ],
+]
+`;
+
+exports[`Table Component should allow expandable cell content: expandable cell content 1`] = `null`;
+
+exports[`Table Component should allow expandable cell content: expanded cell 1`] = `
+
+
+ dolor sit expandable content
+
+
+`;
+
+exports[`Table Component should allow expandable row content: expand row event 1`] = `
+Array [
+ Array [
+ Object {
+ "cellIndex": -1,
+ "data": Object {
+ "data": Object {
+ "hello": "world",
+ },
+ },
+ "isExpanded": true,
+ "rowIndex": 0,
+ "type": "row",
+ },
+ ],
+]
+`;
+
+exports[`Table Component should allow expandable row content: expandable row content 1`] = `null`;
+
+exports[`Table Component should allow expandable row content: expanded row 1`] = `
+
+
+ dolor sit expandable content
+
+
+`;
+
+exports[`Table Component should allow selectable row content: select row content 1`] = `
+Array [
+ ,
+ ,
+ ,
+]
+`;
+
+exports[`Table Component should allow selectable row content: select row input 1`] = `
+Array [
+ Array [
+ Object {
+ "data": Object {
+ "dataLabel": "testing",
+ },
+ "isSelected": true,
+ "rowIndex": 0,
+ "type": "row",
+ },
+ ],
+]
+`;
+
+exports[`Table Component should allow selectable row content: selected row 1`] = `
+Array [
+ ,
+ ,
+ ,
+]
+`;
+
+exports[`Table Component should allow sortable content: sort event 1`] = `
+Array [
+ Array [
+ Object {
+ "cellIndex": 0,
+ "data": Object {},
+ "direction": "asc",
+ },
+ ],
+ Array [
+ Object {
+ "cellIndex": 0,
+ "data": Object {},
+ "direction": "desc",
+ },
+ ],
+]
+`;
+
+exports[`Table Component should allow sortable content: sortable content 1`] = `
+
+
+
+
+
+
+
+
+ lorem ipsum
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Table Component should allow sortable content: sorted column, asc 1`] = `
+
+
+
+
+
+
+
+
+ lorem ipsum
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Table Component should allow sortable content: sorted column, desc 1`] = `
+
+
+
+
+
+
+
+
+ lorem ipsum
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Table Component should allow variations in table layout: ariaLabel and summary 1`] = `
+Object {
+ "aria-label": "lorem ipsum aria-label",
+ "borders": false,
+ "children": Array [
+ false,
+
+
+
+
+ dolor
+
+
+
+
+
+
+ sit
+
+
+
+ ,
+ ],
+ "className": "quipucords-table ",
+ "summary": "lorem ipsum summary",
+ "variant": "compact",
+}
+`;
+
+exports[`Table Component should allow variations in table layout: borders and table header removed 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+ dolor
+
+
+
+
+
+
+
+
+
+
+
+
+ sit
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Table Component should allow variations in table layout: className and variant 1`] = `
+Object {
+ "aria-label": "lorem ipsum aria-label",
+ "borders": false,
+ "children": Array [
+ false,
+
+
+
+
+ dolor
+
+
+
+
+
+
+ sit
+
+
+
+ ,
+ ],
+ "className": "quipucords-table lorem-ipsum-class",
+ "summary": "lorem ipsum summary",
+ "variant": "compact",
+}
+`;
+
+exports[`Table Component should allow variations in table layout: generated rows 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+ lorem ipsum
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ dolor
+
+
+
+
+
+
+
+
+
+
+
+
+ sit
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Table Component should pass child components, nodes when there are no rows: children 1`] = `
+
+
+ Loading...
+
+
+`;
+
+exports[`Table Component should render a basic component: basic 1`] = `
+
+
+
+
+
+`;
diff --git a/src/components/table/__tests__/__snapshots__/tableEmpty.test.js.snap b/src/components/table/__tests__/__snapshots__/tableEmpty.test.js.snap
new file mode 100644
index 00000000..5be3b681
--- /dev/null
+++ b/src/components/table/__tests__/__snapshots__/tableEmpty.test.js.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TableEmpty Component should render a basic component: basic 1`] = `
+
+
+
+
+ t(table.empty-state_title, No results found)
+
+
+ t(table.empty-state_description, Clear all filters and try again.)
+
+
+
+`;
diff --git a/src/components/table/__tests__/__snapshots__/tableHelpers.test.js.snap b/src/components/table/__tests__/__snapshots__/tableHelpers.test.js.snap
new file mode 100644
index 00000000..eb7718a4
--- /dev/null
+++ b/src/components/table/__tests__/__snapshots__/tableHelpers.test.js.snap
@@ -0,0 +1,598 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TableHelpers parseContent should return a consistent output from multiple types: multiple types 1`] = `
+Object {
+ "func": "lorem ipsum",
+ "node":
+ dolor sit
+ ,
+ "null": "null",
+ "obj": "[object Object]",
+ "undefined": "",
+}
+`;
+
+exports[`TableHelpers should have specific functions: tableHelpers 1`] = `
+Object {
+ "parseContent": [Function],
+ "tableHeader": [Function],
+ "tableRows": [Function],
+}
+`;
+
+exports[`TableHelpers tableHeader should return parsed table header settings, props: tableHeader 1`] = `
+Object {
+ "basic": Object {
+ "headerRow": Array [],
+ "headerSelectProps": Object {},
+ },
+ "columnHeaders": Object {
+ "headerRow": Array [
+ Object {
+ "content": "lorem",
+ "key": "bG9yZW0=-0",
+ },
+ Object {
+ "content": "dolor",
+ "data": Object {},
+ "key": "W29iamVjdCBPYmplY3Rd-1",
+ "props": Object {
+ "dataLabel": undefined,
+ "info": undefined,
+ "tooltip": undefined,
+ },
+ },
+ Object {
+ "content": "hello world",
+ "key": "KCkgPT4gJ2hlbGxvIHdvcmxkJw==-2",
+ },
+ Object {
+ "content":
+ hello world
+ ,
+ "key": "W29iamVjdCBPYmplY3Rd-3",
+ },
+ ],
+ "headerSelectProps": Object {},
+ },
+ "isAllSelected": Object {
+ "headerRow": Array [],
+ "headerSelectProps": Object {},
+ },
+ "isRowExpand": Object {
+ "headerRow": Array [
+ Object {
+ "content": "lorem",
+ "key": "bG9yZW0=-0",
+ },
+ Object {
+ "content": "dolor",
+ "data": Object {},
+ "key": "W29iamVjdCBPYmplY3Rd-1",
+ "props": Object {
+ "dataLabel": undefined,
+ "info": undefined,
+ "sort": Object {
+ "columnIndex": 2,
+ "onSort": [Function],
+ "sortBy": Object {
+ "direction": "asc",
+ },
+ },
+ "tooltip": undefined,
+ },
+ },
+ Object {
+ "content": "hello world",
+ "key": "KCkgPT4gJ2hlbGxvIHdvcmxkJw==-2",
+ },
+ ],
+ "headerSelectProps": Object {},
+ },
+ "onSelect": Object {
+ "headerRow": Array [
+ Object {
+ "content": "lorem",
+ "key": "bG9yZW0=-0",
+ },
+ Object {
+ "content": "dolor",
+ "data": Object {},
+ "key": "W29iamVjdCBPYmplY3Rd-1",
+ "props": Object {
+ "dataLabel": undefined,
+ "info": undefined,
+ "tooltip": undefined,
+ },
+ },
+ Object {
+ "content": "hello world",
+ "key": "KCkgPT4gJ2hlbGxvIHdvcmxkJw==-2",
+ },
+ ],
+ "headerSelectProps": Object {
+ "isSelected": false,
+ "onSelect": [Function],
+ },
+ },
+ "onSort": Object {
+ "headerRow": Array [
+ Object {
+ "content": "lorem",
+ "key": "bG9yZW0=-0",
+ },
+ Object {
+ "content": "dolor",
+ "data": Object {},
+ "key": "W29iamVjdCBPYmplY3Rd-1",
+ "props": Object {
+ "dataLabel": undefined,
+ "info": undefined,
+ "sort": Object {
+ "columnIndex": 1,
+ "onSort": [Function],
+ "sortBy": Object {
+ "direction": "asc",
+ },
+ },
+ "tooltip": undefined,
+ },
+ },
+ Object {
+ "content": "hello world",
+ "key": "KCkgPT4gJ2hlbGxvIHdvcmxkJw==-2",
+ },
+ ],
+ "headerSelectProps": Object {},
+ },
+}
+`;
+
+exports[`TableHelpers tableRows should return parsed table body settings, props: tableRows 1`] = `
+Object {
+ "basic": Object {
+ "isAllSelected": true,
+ "isExpandableCell": false,
+ "isExpandableRow": false,
+ "isSelectTable": false,
+ "rows": Array [],
+ },
+ "onExpandCells": Object {
+ "isAllSelected": false,
+ "isExpandableCell": true,
+ "isExpandableRow": false,
+ "isSelectTable": false,
+ "rows": Array [
+ Object {
+ "cells": Array [
+ Object {
+ "content": "lorem",
+ "key": "bG9yZW0=-0-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-0",
+ "rowIndex": 0,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content": "dolor",
+ "expandedContent": "sit",
+ "isExpanded": true,
+ "key": "W29iamVjdCBPYmplY3Rd-1-0",
+ "props": Object {
+ "className": "",
+ "compoundExpand": Object {
+ "isExpanded": true,
+ "onToggle": [Function],
+ },
+ "dataLabel": undefined,
+ "isActionCell": undefined,
+ "noPadding": undefined,
+ "style": Object {},
+ },
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-1",
+ "rowIndex": 1,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content": "hello world",
+ "key": "KCkgPT4gJ2hlbGxvIHdvcmxkJw==-2-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-2",
+ "rowIndex": 2,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content":
+ hello world
+ ,
+ "key": "W29iamVjdCBPYmplY3Rd-3-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-3",
+ "rowIndex": 3,
+ "select": undefined,
+ },
+ ],
+ },
+ "onExpandRows": Object {
+ "isAllSelected": false,
+ "isExpandableCell": false,
+ "isExpandableRow": true,
+ "isSelectTable": false,
+ "rows": Array [
+ Object {
+ "cells": Array [
+ Object {
+ "content": "lorem",
+ "key": "bG9yZW0=-0-0",
+ },
+ ],
+ "data": Object {},
+ "expand": Object {
+ "isExpanded": true,
+ "onToggle": [Function],
+ "rowIndex": 0,
+ },
+ "expandedContent": "ipsum",
+ "key": "W29iamVjdCBPYmplY3Rd-0",
+ "rowIndex": 0,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content": "dolor",
+ "key": "W29iamVjdCBPYmplY3Rd-1-0",
+ "props": Object {
+ "className": "",
+ "dataLabel": undefined,
+ "isActionCell": undefined,
+ "noPadding": undefined,
+ "style": Object {},
+ },
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-1",
+ "rowIndex": 1,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content": "hello world",
+ "key": "KCkgPT4gJ2hlbGxvIHdvcmxkJw==-2-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-2",
+ "rowIndex": 2,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content":
+ hello world
+ ,
+ "key": "W29iamVjdCBPYmplY3Rd-3-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-3",
+ "rowIndex": 3,
+ "select": undefined,
+ },
+ ],
+ },
+ "onSelect": Object {
+ "isAllSelected": false,
+ "isExpandableCell": false,
+ "isExpandableRow": false,
+ "isSelectTable": true,
+ "rows": Array [
+ Object {
+ "cells": Array [
+ Object {
+ "content": "lorem",
+ "key": "bG9yZW0=-0-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-0",
+ "rowIndex": 0,
+ "select": Object {
+ "cells": Array [
+ "lorem",
+ ],
+ "disable": false,
+ "isSelected": true,
+ "onSelect": [Function],
+ "rowIndex": 0,
+ },
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content": "dolor",
+ "key": "W29iamVjdCBPYmplY3Rd-1-0",
+ "props": Object {
+ "className": "",
+ "dataLabel": undefined,
+ "isActionCell": undefined,
+ "noPadding": undefined,
+ "style": Object {},
+ },
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-1",
+ "rowIndex": 1,
+ "select": Object {
+ "cells": Array [
+ Object {
+ "content": "dolor",
+ },
+ ],
+ "disable": true,
+ "isSelected": false,
+ "onSelect": [Function],
+ "rowIndex": 1,
+ },
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content": "hello world",
+ "key": "KCkgPT4gJ2hlbGxvIHdvcmxkJw==-2-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-2",
+ "rowIndex": 2,
+ "select": Object {
+ "cells": Array [
+ [Function],
+ ],
+ "disable": false,
+ "isSelected": false,
+ "onSelect": [Function],
+ "rowIndex": 2,
+ },
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content":
+ hello world
+ ,
+ "key": "W29iamVjdCBPYmplY3Rd-3-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-3",
+ "rowIndex": 3,
+ "select": Object {
+ "cells": Array [
+
+ hello world
+ ,
+ ],
+ "disable": false,
+ "isSelected": false,
+ "onSelect": [Function],
+ "rowIndex": 3,
+ },
+ },
+ ],
+ },
+ "rows": Object {
+ "isAllSelected": false,
+ "isExpandableCell": false,
+ "isExpandableRow": false,
+ "isSelectTable": false,
+ "rows": Array [
+ Object {
+ "cells": Array [
+ Object {
+ "content": "lorem",
+ "key": "bG9yZW0=-0-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-0",
+ "rowIndex": 0,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content": "dolor",
+ "key": "W29iamVjdCBPYmplY3Rd-1-0",
+ "props": Object {
+ "className": "",
+ "dataLabel": undefined,
+ "isActionCell": undefined,
+ "noPadding": undefined,
+ "style": Object {},
+ },
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-1",
+ "rowIndex": 1,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content": "hello world",
+ "key": "KCkgPT4gJ2hlbGxvIHdvcmxkJw==-2-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-2",
+ "rowIndex": 2,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content":
+ hello world
+ ,
+ "key": "W29iamVjdCBPYmplY3Rd-3-0",
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-3",
+ "rowIndex": 3,
+ "select": undefined,
+ },
+ ],
+ },
+ "styling": Object {
+ "isAllSelected": false,
+ "isExpandableCell": false,
+ "isExpandableRow": false,
+ "isSelectTable": false,
+ "rows": Array [
+ Object {
+ "cells": Array [
+ Object {
+ "content": "lorem",
+ "key": "W29iamVjdCBPYmplY3Rd-0-0",
+ "props": Object {
+ "className": "",
+ "dataLabel": undefined,
+ "isActionCell": undefined,
+ "noPadding": undefined,
+ "style": Object {
+ "width": "9px",
+ },
+ },
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-0",
+ "rowIndex": 0,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content": "dolor",
+ "key": "W29iamVjdCBPYmplY3Rd-1-0",
+ "props": Object {
+ "className": "",
+ "dataLabel": undefined,
+ "isActionCell": undefined,
+ "noPadding": undefined,
+ "style": Object {
+ "backgroundColor": "red",
+ "width": "50em",
+ },
+ },
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-1",
+ "rowIndex": 1,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content": "hello world",
+ "key": "W29iamVjdCBPYmplY3Rd-2-0",
+ "props": Object {
+ "className": " pf-m-width-1",
+ "dataLabel": undefined,
+ "isActionCell": undefined,
+ "noPadding": undefined,
+ "style": Object {},
+ },
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-2",
+ "rowIndex": 2,
+ "select": undefined,
+ },
+ Object {
+ "cells": Array [
+ Object {
+ "content":
+ hello world
+ ,
+ "key": "W29iamVjdCBPYmplY3Rd-3-0",
+ "props": Object {
+ "className": "",
+ "dataLabel": undefined,
+ "isActionCell": undefined,
+ "noPadding": undefined,
+ "style": Object {},
+ },
+ },
+ ],
+ "data": Object {},
+ "expand": undefined,
+ "expandedContent": undefined,
+ "key": "W29iamVjdCBPYmplY3Rd-3",
+ "rowIndex": 3,
+ "select": undefined,
+ },
+ ],
+ },
+}
+`;
diff --git a/src/components/table/__tests__/table.test.js b/src/components/table/__tests__/table.test.js
new file mode 100644
index 00000000..98fa290e
--- /dev/null
+++ b/src/components/table/__tests__/table.test.js
@@ -0,0 +1,132 @@
+import React from 'react';
+import { ExpandableRowContent, TableComposable, TableVariant, Th } from '@patternfly/react-table';
+import { Table } from '../table';
+
+describe('Table Component', () => {
+ it('should render a basic component', async () => {
+ const props = {
+ isHeader: true,
+ columnHeaders: ['lorem', 'ipsum', 'dolor', 'sit']
+ };
+
+ const component = await shallowHookComponent();
+ expect(component).toMatchSnapshot('basic');
+ });
+
+ it('should allow variations in table layout', async () => {
+ const props = {
+ isHeader: true,
+ columnHeaders: ['lorem ipsum'],
+ rows: [{ cells: ['dolor'] }, { cells: ['sit'] }]
+ };
+
+ const component = await mountHookComponent();
+ expect(component.find(TableComposable)).toMatchSnapshot('generated rows');
+
+ component.setProps({
+ isBorders: false,
+ isHeader: false
+ });
+ expect(component.find(TableComposable)).toMatchSnapshot('borders and table header removed');
+
+ component.setProps({
+ ariaLabel: 'lorem ipsum aria-label',
+ summary: 'lorem ipsum summary'
+ });
+ expect(component.find(TableComposable).props()).toMatchSnapshot('ariaLabel and summary');
+
+ component.setProps({
+ className: 'lorem-ipsum-class',
+ variant: TableVariant.compact
+ });
+ expect(component.find(TableComposable).props()).toMatchSnapshot('className and variant');
+ });
+
+ it('should allow expandable row content', async () => {
+ const mockOnExpand = jest.fn();
+ const props = {
+ onExpand: mockOnExpand,
+ rows: [
+ { cells: ['dolor'], expandedContent: 'dolor sit expandable content', data: { hello: 'world' } },
+ { cells: ['sit'] }
+ ]
+ };
+
+ const component = await mountHookComponent();
+ expect(component.find(ExpandableRowContent)).toMatchSnapshot('expandable row content');
+
+ component.find('button').first().simulate('click');
+
+ expect(mockOnExpand.mock.calls).toMatchSnapshot('expand row event');
+ expect(component.find(ExpandableRowContent)).toMatchSnapshot('expanded row');
+ });
+
+ it('should allow expandable cell content', async () => {
+ const mockOnExpand = jest.fn();
+ const props = {
+ onExpand: mockOnExpand,
+ rows: [
+ { cells: [{ content: 'dolor', expandedContent: 'dolor sit expandable content' }], data: { hello: 'world' } },
+ { cells: ['sit'] }
+ ]
+ };
+
+ const component = await mountHookComponent();
+ expect(component.find(ExpandableRowContent)).toMatchSnapshot('expandable cell content');
+
+ component.find('button').first().simulate('click', { key: 'Enter' });
+
+ expect(mockOnExpand.mock.calls).toMatchSnapshot('expand cell event');
+ expect(component.find(ExpandableRowContent)).toMatchSnapshot('expanded cell');
+ });
+
+ it('should allow sortable content', async () => {
+ const mockOnSort = jest.fn();
+ const props = {
+ isHeader: true,
+ onSort: mockOnSort,
+ columnHeaders: [{ content: 'lorem ipsum', isSort: true }],
+ rows: [{ cells: ['dolor'] }, { cells: ['sit'] }]
+ };
+
+ const component = await mountHookComponent();
+ expect(component.find(Th).first()).toMatchSnapshot('sortable content');
+
+ component.find('button').first().simulate('click');
+ expect(component.find(Th).first()).toMatchSnapshot('sorted column, asc');
+
+ component.find('button').first().simulate('click');
+ expect(component.find(Th).first()).toMatchSnapshot('sorted column, desc');
+
+ expect(mockOnSort.mock.calls).toMatchSnapshot('sort event');
+ });
+
+ it('should allow selectable row content', async () => {
+ const mockOnSelect = jest.fn();
+ const props = {
+ isHeader: true,
+ columnHeaders: ['lorem ipsum'],
+ onSelect: mockOnSelect,
+ rows: [{ cells: [{ content: 'dolor' }], dataLabel: 'testing' }, { cells: ['sit'] }]
+ };
+
+ const component = await mountHookComponent();
+ expect(component.find('input')).toMatchSnapshot('select row content');
+
+ component.find('input[name="checkrow0"]').simulate('change');
+
+ expect(mockOnSelect.mock.calls).toMatchSnapshot('select row input');
+ expect(component.find('input')).toMatchSnapshot('selected row');
+ });
+
+ it('should pass child components, nodes when there are no rows', async () => {
+ const props = {
+ isHeader: true,
+ columnHeaders: ['lorem ipsum'],
+ rows: []
+ };
+
+ const component = await shallowHookComponent();
+ expect(component).toMatchSnapshot('children');
+ });
+});
diff --git a/src/components/table/__tests__/tableEmpty.test.js b/src/components/table/__tests__/tableEmpty.test.js
new file mode 100644
index 00000000..8ffff92c
--- /dev/null
+++ b/src/components/table/__tests__/tableEmpty.test.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import { TableEmpty } from '../tableEmpty';
+
+describe('TableEmpty Component', () => {
+ it('should render a basic component', async () => {
+ const props = {};
+
+ const component = await shallowHookComponent( );
+ expect(component).toMatchSnapshot('basic');
+ });
+});
diff --git a/src/components/table/__tests__/tableHelpers.test.js b/src/components/table/__tests__/tableHelpers.test.js
new file mode 100644
index 00000000..4559d2b3
--- /dev/null
+++ b/src/components/table/__tests__/tableHelpers.test.js
@@ -0,0 +1,95 @@
+import React from 'react';
+import { tableHelpers, parseContent, tableHeader, tableRows } from '../tableHelpers';
+
+describe('TableHelpers', () => {
+ it('should have specific functions', () => {
+ expect(tableHelpers).toMatchSnapshot('tableHelpers');
+ });
+
+ it('parseContent should return a consistent output from multiple types', () => {
+ expect({
+ func: parseContent(() => 'lorem ipsum'),
+ node: parseContent(dolor sit ),
+ null: parseContent(null),
+ obj: parseContent({ hello: 'world' }),
+ undefined: parseContent(undefined)
+ }).toMatchSnapshot('multiple types');
+ });
+
+ it('tableHeader should return parsed table header settings, props', () => {
+ expect({
+ basic: tableHeader(),
+ isAllSelected: tableHeader({ isAllSelected: true }),
+ columnHeaders: tableHeader({
+ columnHeaders: [
+ 'lorem',
+ { content: 'dolor' },
+ () => 'hello world',
+ hello world
+ ]
+ }),
+ onSelect: tableHeader({
+ onSelect: () => {},
+ columnHeaders: ['lorem', { content: 'dolor' }, () => 'hello world']
+ }),
+ onSort: tableHeader({
+ onSort: () => {},
+ columnHeaders: ['lorem', { content: 'dolor', isSort: true }, () => 'hello world']
+ }),
+ isRowExpand: tableHeader({
+ onSort: () => {},
+ isRowExpand: true,
+ columnHeaders: ['lorem', { content: 'dolor', isSort: true }, () => 'hello world']
+ })
+ }).toMatchSnapshot('tableHeader');
+ });
+
+ it('tableRows should return parsed table body settings, props', () => {
+ expect({
+ basic: tableRows(),
+ rows: tableRows({
+ rows: [
+ { cells: ['lorem'] },
+ { cells: [{ content: 'dolor' }] },
+ { cells: [() => 'hello world'] },
+ { cells: [hello world ] }
+ ]
+ }),
+ styling: tableRows({
+ rows: [
+ { cells: [{ content: 'lorem', width: '9px' }] },
+ { cells: [{ content: 'dolor', style: { backgroundColor: 'red' }, width: '50em' }] },
+ { cells: [{ content: () => 'hello world', width: 1 }] },
+ { cells: [{ content: hello world , width: 11 }] }
+ ]
+ }),
+ onExpandCells: tableRows({
+ onExpand: () => {},
+ rows: [
+ { cells: ['lorem'] },
+ { cells: [{ content: 'dolor', expandedContent: 'sit', isExpanded: true }] },
+ { cells: [() => 'hello world'] },
+ { cells: [hello world ] }
+ ]
+ }),
+ onExpandRows: tableRows({
+ onExpand: () => {},
+ rows: [
+ { cells: ['lorem'], expandedContent: 'ipsum', isExpanded: true },
+ { cells: [{ content: 'dolor' }] },
+ { cells: [() => 'hello world'] },
+ { cells: [hello world ] }
+ ]
+ }),
+ onSelect: tableRows({
+ onSelect: () => {},
+ rows: [
+ { cells: ['lorem'], isSelected: true },
+ { cells: [{ content: 'dolor' }], isDisabled: true },
+ { cells: [() => 'hello world'] },
+ { cells: [hello world ] }
+ ]
+ })
+ }).toMatchSnapshot('tableRows');
+ });
+});
diff --git a/src/components/table/table.js b/src/components/table/table.js
new file mode 100644
index 00000000..0108ac4f
--- /dev/null
+++ b/src/components/table/table.js
@@ -0,0 +1,492 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { useShallowCompareEffect } from 'react-use';
+import _cloneDeep from 'lodash/cloneDeep';
+import { Grid, GridItem } from '@patternfly/react-core';
+import {
+ ExpandableRowContent,
+ SortByDirection,
+ TableComposable,
+ TableVariant,
+ Tbody,
+ Td,
+ Th,
+ Thead,
+ Tr
+} from '@patternfly/react-table';
+import { TableEmpty } from './tableEmpty';
+import { tableHelpers } from './tableHelpers';
+
+/**
+ * FixMe: PF bug for select column. PF requires a Th used for select field in the primary Thead...
+ * BUT also allows a partially working Td. Any attempt to update the Td selected object props is
+ * met with a partially-functioning field, hair pulling, and the question "is my state working?"
+ * ... it is, PF is the problem, this is a bug. HTML markup does allow the use of both td and th within
+ * thead and tbody, and not every cell in a thead requires the use of th. Solutions include
+ * - minimally updating the documentation to reflect that a Th is ABSOLUTELY required!
+ * - allow Td cells the same functionality as Th in Thead
+ * - completely warn/block the ability to use Td in the Thead component
+ */
+/**
+ * A PF Composable table wrapper
+ *
+ * @param {object} props
+ * @param {string} props.ariaLabel
+ * @param {React.ReactNode} props.children
+ * @param {string} props.className
+ * @param {Array} props.columnHeaders
+ * @param {object} props.componentClassNames
+ * @param {boolean} props.isBorders
+ * @param {boolean} props.isHeader
+ * @param {Function} props.onSelect
+ * @param {Function} props.onSort
+ * @param {Function} props.onExpand
+ * @param {Array} props.rows
+ * @param {string} props.summary
+ * @param {string} props.variant
+ * @returns {React.ReactNode}
+ */
+const Table = ({
+ ariaLabel,
+ children,
+ className,
+ columnHeaders,
+ componentClassNames,
+ isBorders,
+ isHeader,
+ onSelect,
+ onSort,
+ onExpand,
+ rows,
+ summary,
+ variant
+}) => {
+ const [updatedHeaderAndRows, setUpdatedHeaderAndRows] = useState({});
+ const [updatedIsExpandableRow, setUpdatedIsExpandableRow] = useState(false);
+ const [updatedIsExpandableCell, setUpdatedIsExpandableCell] = useState(false);
+ const [updatedIsSelectTable, setUpdatedIsSelectTable] = useState(false);
+
+ /**
+ * Apply an onExpand handler.
+ *
+ * @param {object} params
+ * @param {string} params.type
+ * @param {boolean} params.isExpanded
+ * @param {number} params.rowIndex
+ * @param {number} params.cellIndex
+ * @param {*|object} params.data
+ */
+ const onExpandTable = ({ type, isExpanded, rowIndex, cellIndex, data } = {}) => {
+ if (type === 'compound') {
+ setUpdatedHeaderAndRows(prevState => {
+ const nextBodyRows = [...prevState.bodyRows];
+ const nextBodyRowCells = nextBodyRows?.[rowIndex].cells.map(({ props: cellProps, ...cell }) => {
+ const updatedCompoundExpand = cellProps?.compoundExpand;
+
+ if (updatedCompoundExpand) {
+ updatedCompoundExpand.isExpanded = false;
+ }
+
+ return { ...cell, props: { ...cellProps, compoundExpand: updatedCompoundExpand } };
+ });
+
+ nextBodyRowCells[cellIndex].props.compoundExpand.isExpanded = isExpanded;
+ nextBodyRows[rowIndex].cells = nextBodyRowCells;
+
+ return {
+ ...prevState,
+ bodyRows: nextBodyRows
+ };
+ });
+ } else {
+ setUpdatedHeaderAndRows(prevState => {
+ const nextBodyRows = [...prevState.bodyRows];
+ nextBodyRows[rowIndex].expand.isExpanded = isExpanded;
+
+ return {
+ ...prevState,
+ bodyRows: nextBodyRows
+ };
+ });
+ }
+
+ if (typeof onExpand === 'function') {
+ onExpand({
+ type,
+ rowIndex,
+ cellIndex: (type === 'row' && -1) || cellIndex,
+ isExpanded,
+ data: _cloneDeep(data)
+ });
+ }
+ };
+
+ /**
+ * Apply an onSelect handler.
+ *
+ * @param {object} params
+ * @param {string} params.type
+ * @param {boolean} params.isSelected
+ * @param {number} params.rowIndex
+ * @param {*|object} params.data
+ */
+ const onSelectTable = ({ type, isSelected, rowIndex, data } = {}) => {
+ if (type === 'all') {
+ setUpdatedHeaderAndRows(prevState => {
+ const nextBodyRows = prevState.bodyRows?.map(row => ({
+ ...row,
+ select: { ...row.select, isSelected }
+ }));
+
+ const nextHeaderSelectProps = prevState.headerSelectProps;
+ nextHeaderSelectProps.isSelected = isSelected;
+
+ return {
+ ...prevState,
+ bodyRows: nextBodyRows,
+ headerSelectProps: nextHeaderSelectProps
+ };
+ });
+ } else {
+ setUpdatedHeaderAndRows(prevState => {
+ const nextBodyRows = prevState.bodyRows?.map(row => row);
+ nextBodyRows[rowIndex].select.isSelected = isSelected;
+
+ const nextHeaderSelectProps = prevState.headerSelectProps;
+ nextHeaderSelectProps.isSelected =
+ nextBodyRows.filter(row => row.select.isSelected === true).length === nextBodyRows.length;
+
+ return {
+ ...prevState,
+ bodyRows: nextBodyRows,
+ headerSelectProps: nextHeaderSelectProps
+ };
+ });
+ }
+
+ onSelect({
+ type,
+ rowIndex,
+ isSelected,
+ data: _cloneDeep(data)
+ });
+ };
+
+ /**
+ * Apply an onSort handler.
+ *
+ * @param {object} params
+ * @param {number} params.cellIndex
+ * @param {string} params.direction
+ * @param {number} params.originalIndex
+ * @param {*|object} params.data
+ */
+ const onSortTable = ({ cellIndex, direction, originalIndex, data } = {}) => {
+ setUpdatedHeaderAndRows(prevState => {
+ const nextHeaderRow = prevState.headerRow.map((headerCell, index) => {
+ const updatedHeaderCell = headerCell;
+
+ if (updatedHeaderCell?.props?.sort) {
+ delete updatedHeaderCell.props.sort.sortBy.index;
+
+ if (index === originalIndex) {
+ updatedHeaderCell.props.sort.sortBy.index = cellIndex;
+ updatedHeaderCell.props.sort.sortBy.direction = direction;
+ }
+ }
+
+ return updatedHeaderCell;
+ });
+
+ return {
+ ...prevState,
+ headerRow: nextHeaderRow
+ };
+ });
+
+ onSort({
+ cellIndex: originalIndex,
+ direction,
+ data: _cloneDeep(data)
+ });
+ };
+
+ useShallowCompareEffect(() => {
+ const {
+ isAllSelected: parsedIsAllSelected,
+ isExpandableCell: parsedIsExpandableCell,
+ isExpandableRow: parsedIsExpandableRow,
+ isSelectTable: parsedIsSelectTable,
+ rows: parsedRows
+ } = tableHelpers.tableRows({
+ onExpand: onExpandTable,
+ onSelect: typeof onSelect === 'function' && onSelectTable,
+ rows
+ });
+ const { headerRow: parsedHeaderRow, headerSelectProps: parsedHeaderSelectProps } = tableHelpers.tableHeader({
+ columnHeaders,
+ isAllSelected: parsedIsAllSelected,
+ onSelect: typeof onSelect === 'function' && onSelectTable,
+ onSort: typeof onSort === 'function' && onSortTable,
+ parsedRows
+ });
+
+ setUpdatedIsExpandableRow(parsedIsExpandableRow);
+ setUpdatedIsSelectTable(parsedIsSelectTable);
+ setUpdatedIsExpandableCell(parsedIsExpandableCell);
+ setUpdatedHeaderAndRows({
+ headerRow: parsedHeaderRow,
+ bodyRows: parsedRows,
+ headerSelectProps: parsedHeaderSelectProps
+ });
+ }, [columnHeaders, onExpand, onExpandTable, onSelect, onSelectTable, onSort, onSortTable, rows]);
+
+ /**
+ * Apply settings, return primary thead.
+ *
+ * @returns {React.ReactNode}
+ */
+ const renderHeader = () => (
+
+
+ {updatedIsExpandableRow && }
+ {updatedIsSelectTable && (
+
+ )}
+ {updatedHeaderAndRows?.headerRow.map(({ key: cellKey, content, props, sort }) => (
+
+ {content}
+
+ ))}
+
+
+ );
+
+ /**
+ * Apply settings, return tbody(s).
+ *
+ * @returns {React.ReactNode}
+ */
+ const renderBody = () => {
+ const BodyWrapper = ((updatedIsExpandableCell || updatedIsExpandableRow) && React.Fragment) || Tbody;
+
+ return (
+
+ {updatedHeaderAndRows?.bodyRows?.map(({ key: rowKey, cells, expand, select, expandedContent }) => {
+ const expandedCell =
+ (updatedIsExpandableCell && cells.find(cell => cell?.props?.compoundExpand?.isExpanded === true)) ||
+ undefined;
+ const expandedRow = (updatedIsExpandableRow && expand?.isExpanded === true) || undefined;
+
+ const CellWrapper = ((updatedIsExpandableCell || updatedIsExpandableRow) && Tbody) || React.Fragment;
+ const cellWrapperProps =
+ (updatedIsExpandableCell && { isExpanded: expandedCell?.props?.compoundExpand?.isExpanded === true }) ||
+ (updatedIsExpandableRow && { isExpanded: expand?.isExpanded === true }) ||
+ undefined;
+
+ return (
+
+
+ {expand && (
+
+ )}
+ {select && (
+
+ )}
+ {cells.map(({ key: cellKey, content, isTHeader, props: cellProps = {} }) => {
+ const WrapperCell = (isTHeader && Th) || Td;
+
+ return (
+
+ {content}
+
+ );
+ })}
+
+ {updatedIsExpandableRow && expandedRow && (
+
+
+
+ {expandedContent}
+
+
+
+ )}
+ {updatedIsExpandableCell && expandedCell && (
+
+
+
+
+ {(typeof expandedCell.expandedContent === 'function' && expandedCell.expandedContent()) ||
+ expandedCell.expandedContent}
+
+
+
+
+ )}
+
+ );
+ })}
+
+ );
+ };
+
+ /**
+ * Return empty results display.
+ *
+ * @returns {React.ReactNode}
+ */
+ const renderEmpty = () => children || ;
+
+ return (
+
+
+ {(updatedHeaderAndRows?.bodyRows?.length && (
+
+ {isHeader && renderHeader()}
+ {renderBody()}
+
+ )) ||
+ renderEmpty()}
+
+
+ );
+};
+
+/**
+ * Prop types
+ *
+ * @type {{componentClassNames: object, summary: string, onSort: Function, onExpand: Function, className: string, rows: Array,
+ * isBorders: boolean, ariaLabel: string, onSelect: Function, columnHeaders: Array, children: React.ReactNode, isHeader: boolean,
+ * variant: string}}
+ */
+Table.propTypes = {
+ ariaLabel: PropTypes.string,
+ children: PropTypes.node,
+ className: PropTypes.string,
+ columnHeaders: PropTypes.arrayOf(
+ PropTypes.oneOfType([
+ PropTypes.func,
+ PropTypes.node,
+ PropTypes.shape({
+ content: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired,
+ isSort: PropTypes.bool,
+ isSortActive: PropTypes.bool,
+ sortDirection: PropTypes.oneOf([...Object.values(SortByDirection)])
+ })
+ ])
+ ),
+ componentClassNames: PropTypes.shape({
+ table: PropTypes.string,
+ td: PropTypes.string,
+ tdAction: PropTypes.string,
+ tdSelect: PropTypes.string,
+ th: PropTypes.string,
+ tr: PropTypes.string,
+ trExpand: PropTypes.string,
+ trExpanded: PropTypes.string,
+ trExpandedContent: PropTypes.string,
+ tdExpand: PropTypes.string,
+ tdExpanded: PropTypes.string,
+ tdExpandedWrapper: PropTypes.string,
+ tdExpandedContent: PropTypes.string
+ }),
+ isBorders: PropTypes.bool,
+ isHeader: PropTypes.bool,
+ onExpand: PropTypes.func,
+ onSelect: PropTypes.func,
+ onSort: PropTypes.func,
+ rows: PropTypes.arrayOf(
+ PropTypes.shape({
+ cells: PropTypes.arrayOf(
+ PropTypes.oneOfType([
+ PropTypes.func,
+ PropTypes.node,
+ PropTypes.instanceOf(Date),
+ PropTypes.shape({
+ content: PropTypes.oneOfType([PropTypes.func, PropTypes.node, PropTypes.instanceOf(Date)]).isRequired,
+ isTHeader: PropTypes.bool,
+ isExpanded: PropTypes.bool,
+ expandedContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func])
+ })
+ ])
+ ),
+ isDisabled: PropTypes.bool,
+ isExpanded: PropTypes.bool,
+ isSelected: PropTypes.bool,
+ expandedContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func])
+ })
+ ),
+ summary: PropTypes.string,
+ variant: PropTypes.oneOf([...Object.values(TableVariant)])
+};
+
+/**
+ * Default props
+ *
+ * @type {{componentClassNames: {td: string, trExpanded: string, tdExpanded: string, th: string, trExpand: string,
+ * trExpandedContent: string, tdExpandedContent: string, table: string, tr: string, tdExpand: string}, summary: null,
+ * onSort: null, onExpand: null, className: string, rows: *[], isBorders: boolean, ariaLabel: null, onSelect: null,
+ * columnHeaders: *[], children: null, isHeader: boolean, variant: TableVariant.compact}}
+ */
+Table.defaultProps = {
+ ariaLabel: null,
+ children: null,
+ className: '',
+ columnHeaders: [],
+ componentClassNames: {
+ table: 'quipucords-table',
+ td: 'quipucords-table__td',
+ tdAction: 'quipucords-table__td-action',
+ tdSelect: 'quipucords-table__td-select',
+ th: 'quipucords-table__th',
+ tr: 'quipucords-table__tr',
+ trExpand: 'quipucords-table__tr-expand',
+ trExpanded: 'quipucords-table__tr-expand-expanded',
+ trExpandedContent: 'quipucords-table__tr-expand-content',
+ tdExpand: 'quipucords-table__td-expand',
+ tdExpanded: 'quipucords-table__td-expand-expanded',
+ tdExpandedWrapper: 'quipucords-table__td-expand-wrapper',
+ tdExpandedContent: 'quipucords-table__td-expand-content'
+ },
+ isBorders: true,
+ isHeader: false,
+ onExpand: null,
+ onSelect: null,
+ onSort: null,
+ rows: [],
+ summary: null,
+ variant: TableVariant.compact
+};
+
+export { Table as default, Table };
diff --git a/src/components/table/tableEmpty.js b/src/components/table/tableEmpty.js
new file mode 100644
index 00000000..f838a886
--- /dev/null
+++ b/src/components/table/tableEmpty.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Bullseye, EmptyState, EmptyStateVariant, EmptyStateIcon, EmptyStateBody, Title } from '@patternfly/react-core';
+import { SearchIcon } from '@patternfly/react-icons';
+import { translate } from '../i18n/i18n';
+
+const TableEmpty = ({ t }) => (
+
+
+
+
+ {t('table.empty-state_title', 'No results found')}
+
+ {t('table.empty-state_description', 'Clear all filters and try again.')}
+
+
+);
+
+TableEmpty.propTypes = {
+ t: PropTypes.func
+};
+
+TableEmpty.defaultProps = {
+ t: translate
+};
+
+export { TableEmpty as default, TableEmpty };
diff --git a/src/components/table/tableHelpers.js b/src/components/table/tableHelpers.js
new file mode 100644
index 00000000..f1944b74
--- /dev/null
+++ b/src/components/table/tableHelpers.js
@@ -0,0 +1,235 @@
+import React from 'react';
+import { SortByDirection } from '@patternfly/react-table';
+
+/**
+ * Allow additional content to display in cells.
+ *
+ * @param {React.ReactNode|Function|object|*} content
+ * @returns {*|string}
+ */
+const parseContent = content =>
+ (React.isValidElement(content) && content) ||
+ (typeof content === 'function' && content()) ||
+ (typeof content === 'object' && `${content}`) ||
+ content ||
+ '';
+
+/**
+ * Parse table header settings, props.
+ *
+ * @param {object} params
+ * @param {Array} params.columnHeaders
+ * @param {boolean} params.isAllSelected
+ * @param {boolean} params.isRowExpand
+ * @param {Array} params.parsedRows
+ * @param {Function} params.onSelect
+ * @param {Function} params.onSort
+ * @returns {{headerRow: *[], headerSelectProps: {}}}
+ */
+const tableHeader = ({
+ columnHeaders = [],
+ isAllSelected = false,
+ isRowExpand,
+ parsedRows = [],
+ onSelect,
+ onSort
+} = {}) => {
+ const updatedColumnHeaders = [];
+ const updatedHeaderSelectProps = {};
+ const isSelectTable = typeof onSelect === 'function';
+
+ if (isSelectTable) {
+ const parsedRowData = parsedRows.map(({ data }) => data || {});
+ updatedHeaderSelectProps.onSelect = (_event, isSelected) =>
+ onSelect({ data: parsedRowData, isSelected, rowIndex: -1, type: 'all' });
+ updatedHeaderSelectProps.isSelected = isAllSelected;
+ }
+
+ columnHeaders.forEach((columnHeader, index) => {
+ const key = `${window.btoa(columnHeader)}-${index}`;
+
+ if (columnHeader?.content !== undefined) {
+ const {
+ isSort,
+ isSortActive,
+ sortDirection = SortByDirection.asc,
+ content,
+ dataLabel,
+ info,
+ tooltip,
+ ...headerCellData
+ } = columnHeader;
+ const tempColumnHeader = {
+ key,
+ content: parseContent(content),
+ props: {
+ dataLabel,
+ info,
+ tooltip
+ },
+ data: headerCellData
+ };
+
+ if (typeof onSort === 'function' && (isSort === true || isSortActive === true)) {
+ let updatedColumnIndex = index;
+
+ if (isRowExpand) {
+ updatedColumnIndex += 1;
+ }
+
+ if (isSelectTable) {
+ updatedColumnIndex += 1;
+ }
+
+ tempColumnHeader.props.sort = {
+ columnIndex: updatedColumnIndex,
+ sortBy: {},
+ onSort: (_event, _colIndex, direction) =>
+ onSort({ cellIndex: updatedColumnIndex, data: headerCellData, direction, originalIndex: index })
+ };
+
+ if (isSortActive) {
+ tempColumnHeader.props.sort.sortBy.index = updatedColumnIndex;
+ }
+
+ tempColumnHeader.props.sort.sortBy.direction = sortDirection;
+ }
+
+ updatedColumnHeaders.push(tempColumnHeader);
+ } else {
+ updatedColumnHeaders.push({
+ key,
+ content: parseContent(columnHeader)
+ });
+ }
+ });
+
+ return {
+ headerRow: updatedColumnHeaders,
+ headerSelectProps: updatedHeaderSelectProps
+ };
+};
+
+/**
+ * Parse table body settings, props.
+ *
+ * @param {object} params
+ * @param {Function} params.onExpand
+ * @param {Function} params.onSelect
+ * @param {Array} params.rows
+ * @returns {{isExpandableCell: boolean, isSelectTable: boolean, isExpandableRow: boolean, isAllSelected: boolean, rows: *[]}}
+ */
+const tableRows = ({ onExpand, onSelect, rows = [] } = {}) => {
+ const updatedRows = [];
+ const isSelectTable = typeof onSelect === 'function';
+ let isExpandableRow = false;
+ let isExpandableCell = false;
+ let selectedRows = 0;
+
+ rows.forEach(({ cells, isDisabled = false, isExpanded = false, isSelected = false, expandedContent, ...rowData }) => {
+ const rowObj = {
+ key: undefined,
+ cells: [],
+ select: undefined,
+ expand: undefined,
+ expandedContent,
+ data: rowData
+ };
+ updatedRows.push(rowObj);
+ rowObj.rowIndex = updatedRows.length - 1;
+ rowObj.key = `${window.btoa(rowObj)}-${rowObj.rowIndex}`;
+
+ if (isSelectTable) {
+ const updatedIsSelected = isSelected ?? false;
+
+ if (updatedIsSelected === true) {
+ selectedRows += 1;
+ }
+
+ rowObj.select = {
+ cells,
+ rowIndex: rowObj.rowIndex,
+ onSelect: (_event, isRowSelected) =>
+ onSelect({ data: rowObj.data, isSelected: isRowSelected, rowIndex: rowObj.rowIndex, type: 'row' }),
+ isSelected: updatedIsSelected,
+ disable: isDisabled || false
+ };
+ }
+
+ if (expandedContent && typeof onExpand === 'function') {
+ isExpandableRow = true;
+
+ rowObj.expand = {
+ rowIndex: rowObj.rowIndex,
+ isExpanded,
+ onToggle: (_event, rowIndex, isRowToggleExpanded) =>
+ onExpand({
+ data: rowObj.data,
+ isExpanded: isRowToggleExpanded,
+ rowIndex: rowObj.rowIndex,
+ type: 'row'
+ })
+ };
+ }
+
+ cells?.forEach((cell, cellIndex) => {
+ const cellKey = `${window.btoa(cell)}-${rowObj.rowIndex}-${cellIndex}`;
+ if (cell?.content !== undefined) {
+ const { className, content, dataLabel, isActionCell, noPadding, width, style, ...remainingProps } = cell;
+ const cellProps = { className: className || '', dataLabel, isActionCell, noPadding, style: style || {} };
+ let updatedWidthClassName;
+
+ // FixMe: PF doesn't appear to apply cell width classNames when less than 10
+ if (width < 10) {
+ updatedWidthClassName = `pf-m-width-${width}`;
+ }
+
+ if (typeof width === 'string' || style) {
+ cellProps.style = { ...cellProps.style, width };
+ } else if (updatedWidthClassName) {
+ cellProps.className = `${cellProps.className || ''} ${updatedWidthClassName}`;
+ }
+
+ if (!isExpandableRow && cell?.expandedContent && typeof onExpand === 'function') {
+ isExpandableCell = true;
+ const updateIsExpanded = cell?.isExpanded ?? false;
+
+ cellProps.compoundExpand = {
+ isExpanded: updateIsExpanded,
+ onToggle: (_event, rowIndex, isRowToggleExpanded, isCellToggleExpanded) =>
+ onExpand({
+ cellIndex,
+ data: rowObj.data,
+ isExpanded: !isCellToggleExpanded,
+ rowIndex: rowObj.rowIndex,
+ type: 'compound'
+ })
+ };
+ }
+
+ rowObj.cells.push({ ...remainingProps, content: parseContent(content), key: cellKey, props: cellProps });
+ } else {
+ rowObj.cells.push({
+ key: cellKey,
+ content: parseContent(cell)
+ });
+ }
+ });
+ });
+
+ return {
+ isAllSelected: selectedRows === rows.length,
+ isExpandableRow,
+ isExpandableCell,
+ isSelectTable,
+ rows: updatedRows
+ };
+};
+
+const tableHelpers = {
+ parseContent,
+ tableHeader,
+ tableRows
+};
+
+export { tableHelpers as default, tableHelpers, parseContent, tableHeader, tableRows };
diff --git a/src/components/toastNotificationsList/__tests__/__snapshots__/toastNotificationsList.test.js.snap b/src/components/toastNotificationsList/__tests__/__snapshots__/toastNotificationsList.test.js.snap
index 6b6757b9..9302aac8 100644
--- a/src/components/toastNotificationsList/__tests__/__snapshots__/toastNotificationsList.test.js.snap
+++ b/src/components/toastNotificationsList/__tests__/__snapshots__/toastNotificationsList.test.js.snap
@@ -1,7 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ToastNotificationsList Component should shallow render a basic component 1`] = `
-
+exports[`ToastNotificationsList Component should handle toast variations: variations 1`] = `
+
+
+ }
+ key="key-"
+ onMouseEnter={[Function]}
+ onMouseLeave={[Function]}
+ onTimeout={[Function]}
+ timeout={8000}
+ title="Lorem ipsum"
+ truncateTitle={2}
+ />
+
+ }
+ key="key-"
+ onMouseEnter={[Function]}
+ onMouseLeave={[Function]}
+ onTimeout={[Function]}
+ timeout={8000}
+ title="Dolor sit"
+ truncateTitle={2}
+ variant="success"
+ />
+
+ }
+ key="key-"
+ onMouseEnter={[Function]}
+ onMouseLeave={[Function]}
+ onTimeout={[Function]}
+ timeout={8000}
+ title="Dolor sit"
+ truncateTitle={2}
+ variant="danger"
+ >
+ Lorem ipsum
+
+
+ }
+ key="key-"
+ onMouseEnter={[Function]}
+ onMouseLeave={[Function]}
+ onTimeout={[Function]}
+ timeout={false}
+ title="PAUSED, Dolor sit"
+ truncateTitle={2}
+ variant="info"
+ />
+
`;
+
+exports[`ToastNotificationsList Component should shallow render a basic component: basic 1`] = ` `;
diff --git a/src/components/toastNotificationsList/__tests__/toastNotificationsList.test.js b/src/components/toastNotificationsList/__tests__/toastNotificationsList.test.js
index a4020423..cc91ef11 100644
--- a/src/components/toastNotificationsList/__tests__/toastNotificationsList.test.js
+++ b/src/components/toastNotificationsList/__tests__/toastNotificationsList.test.js
@@ -2,20 +2,66 @@ import React from 'react';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { shallow } from 'enzyme';
-import ToastNotificationsList from '../toastNotificationsList';
+import { AlertVariant } from '@patternfly/react-core';
+import { ConnectedToastNotificationsList, ToastNotificationsList } from '../toastNotificationsList';
describe('ToastNotificationsList Component', () => {
const generateEmptyStore = () => configureMockStore()({ toastNotifications: {} });
it('should shallow render a basic component', () => {
const store = generateEmptyStore();
- const props = { show: true };
- const wrapper = shallow(
+ const props = {};
+ const component = shallow(
-
+
);
- expect(wrapper.find(ToastNotificationsList)).toMatchSnapshot();
+ expect(component.find(ConnectedToastNotificationsList)).toMatchSnapshot('basic');
+ });
+
+ it('should handle toast variations', () => {
+ const props = {
+ toasts: [
+ {
+ alertType: undefined,
+ header: undefined,
+ message: 'Lorem ipsum',
+ removed: undefined,
+ paused: undefined
+ },
+ {
+ alertType: AlertVariant.success,
+ header: 'Dolor sit',
+ message: undefined,
+ removed: undefined,
+ paused: undefined
+ },
+ {
+ alertType: AlertVariant.danger,
+ header: 'Dolor sit',
+ message: 'Lorem ipsum',
+ removed: undefined,
+ paused: undefined
+ },
+ {
+ alertType: AlertVariant.info,
+ header: 'REMOVED, Dolor sit',
+ message: undefined,
+ removed: true,
+ paused: undefined
+ },
+ {
+ alertType: AlertVariant.info,
+ header: 'PAUSED, Dolor sit',
+ message: undefined,
+ removed: undefined,
+ paused: true
+ }
+ ]
+ };
+ const component = shallow( );
+
+ expect(component).toMatchSnapshot('variations');
});
});
diff --git a/src/components/toastNotificationsList/toastNotificationsList.js b/src/components/toastNotificationsList/toastNotificationsList.js
index 5be543f4..169a294d 100644
--- a/src/components/toastNotificationsList/toastNotificationsList.js
+++ b/src/components/toastNotificationsList/toastNotificationsList.js
@@ -1,16 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { ToastNotificationList, TimedToastNotification } from 'patternfly-react';
+import { Alert, AlertActionCloseButton, AlertGroup, AlertVariant } from '@patternfly/react-core';
import { connect, reduxTypes, store } from '../../redux';
import helpers from '../../common/helpers';
+/**
+ * Toast notifications list: operates by allowing mutation of the passed/original toast object from state.
+ */
class ToastNotificationsList extends React.Component {
- onHover = () => {
- store.dispatch({ type: reduxTypes.toastNotifications.TOAST_PAUSE });
+ onHover = toast => {
+ store.dispatch({
+ type: reduxTypes.toastNotifications.TOAST_PAUSE,
+ toast
+ });
};
- onLeave = () => {
- store.dispatch({ type: reduxTypes.toastNotifications.TOAST_RESUME });
+ onLeave = toast => {
+ store.dispatch({
+ type: reduxTypes.toastNotifications.TOAST_RESUME,
+ toast
+ });
};
onDismiss = toast => {
@@ -21,46 +30,50 @@ class ToastNotificationsList extends React.Component {
};
render() {
- const { toasts, paused } = this.props;
+ const { toasts, timeout } = this.props;
return (
-
- {toasts &&
- toasts.map((toast, index) => {
- if (!toast.removed) {
- return (
- this.onDismiss(toast)}
- onMouseEnter={this.onHover}
- onMouseLeave={this.onLeave}
- >
-
- {toast.header}
- {toast.message}
-
-
- );
- }
-
- return null;
- })}
-
+
+ {toasts?.map(toast => {
+ if (!toast.removed) {
+ return (
+ this.onDismiss(toast)}
+ variant={toast.alertType}
+ actionClose={ this.onDismiss(toast)} />}
+ key={helpers.generateId('key')}
+ onMouseEnter={() => this.onHover(toast)}
+ onMouseLeave={() => this.onLeave(toast)}
+ >
+ {(toast.header && toast.message) || ''}
+
+ );
+ }
+ return null;
+ })}
+
);
}
}
ToastNotificationsList.propTypes = {
- toasts: PropTypes.array,
- paused: PropTypes.bool
+ toasts: PropTypes.arrayOf(
+ PropTypes.shape({
+ alertType: PropTypes.oneOf([...Object.values(AlertVariant)]),
+ header: PropTypes.node,
+ message: PropTypes.node,
+ removed: PropTypes.bool
+ })
+ ),
+ timeout: PropTypes.number
};
ToastNotificationsList.defaultProps = {
toasts: [],
- paused: false
+ timeout: helpers.TOAST_NOTIFICATIONS_TIMEOUT
};
const mapStateToProps = state => ({ ...state.toastNotifications });
diff --git a/src/components/tooltip/__tests__/__snapshots__/tooltip.test.js.snap b/src/components/tooltip/__tests__/__snapshots__/tooltip.test.js.snap
index 2a4fa88a..56e3d91f 100644
--- a/src/components/tooltip/__tests__/__snapshots__/tooltip.test.js.snap
+++ b/src/components/tooltip/__tests__/__snapshots__/tooltip.test.js.snap
@@ -2,90 +2,219 @@
exports[`Tooltip Component should render a popover: popover 1`] = `
-
- hello world
-
- }
- placement="top"
- rootClose={true}
- trigger={
- Array [
- "hover",
- ]
- }
+
-
+
+
+
+ hello world
+
+
+
+ }
+ 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={
+
+ Test popover
+
+ }
+ zIndex={9999}
>
- Test popover
-
-
+
+
+ Test popover
+
+
+
+
`;
exports[`Tooltip Component should render a tooltip: tooltip 1`] = `
-
- hello world
-
- }
- placement="top"
- rootClose={true}
- trigger={
- Array [
- "hover",
- ]
- }
+
-
+
+
+ hello world
+
+
+ }
+ 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={
+
+ Test tooltip
+
+ }
+ zIndex={9999}
>
- Test tooltip
-
-
+
+
+ Test tooltip
+
+
+
+
`;
diff --git a/src/components/tooltip/__tests__/tooltip.test.js b/src/components/tooltip/__tests__/tooltip.test.js
index 23c3831c..ecd4b78e 100644
--- a/src/components/tooltip/__tests__/tooltip.test.js
+++ b/src/components/tooltip/__tests__/tooltip.test.js
@@ -6,7 +6,7 @@ describe('Tooltip Component', () => {
it('should render a tooltip', () => {
const props = {
id: 'test',
- tooltip: 'hello world'
+ content: 'hello world'
};
const component = mount(Test tooltip );
@@ -17,7 +17,8 @@ describe('Tooltip Component', () => {
it('should render a popover', () => {
const props = {
id: 'test',
- popover: 'hello world'
+ isPopover: true,
+ content: 'hello world'
};
const component = mount(Test popover );
diff --git a/src/components/tooltip/tooltip.js b/src/components/tooltip/tooltip.js
index 7c7bb3a5..698b0f05 100644
--- a/src/components/tooltip/tooltip.js
+++ b/src/components/tooltip/tooltip.js
@@ -1,53 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Icon, OverlayTrigger, Popover, Tooltip as PFTooltip } from 'patternfly-react';
+import { Icon } from 'patternfly-react';
+import { Popover, Tooltip as PFTooltip } from '@patternfly/react-core';
import helpers from '../../common/helpers';
-const Tooltip = ({ children, tooltip, id, placement, popover, rootClose, trigger, delayShow, ...props }) => {
+const Tooltip = ({ children, id, placement, isPopover, content, delayShow }) => {
const setId = id || helpers.generateId();
- const tooltipPopover = popover ? (
-
- {popover}
-
- ) : (
-
- {tooltip || 'example tooltip'}
-
- );
+ if (isPopover) {
+ return (
+
+ {children || }
+
+ );
+ }
return (
-
- {children || }
-
+
+ {children || }
+
);
};
Tooltip.propTypes = {
children: PropTypes.node,
- popover: PropTypes.node,
- tooltip: PropTypes.node,
+ isPopover: PropTypes.bool,
+ content: PropTypes.node,
id: PropTypes.string,
placement: PropTypes.string,
- rootClose: PropTypes.bool,
- trigger: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
delayShow: PropTypes.number
};
Tooltip.defaultProps = {
children: null,
- popover: null,
- tooltip: null,
+ isPopover: false,
+ content: null,
id: null,
placement: 'top',
- rootClose: true,
- trigger: ['hover'],
delayShow: 500
};
diff --git a/src/components/viewPaginationRow/__tests__/__snapshots__/viewPaginationRow.test.js.snap b/src/components/viewPaginationRow/__tests__/__snapshots__/viewPaginationRow.test.js.snap
index 2aee9a92..b388ed63 100644
--- a/src/components/viewPaginationRow/__tests__/__snapshots__/viewPaginationRow.test.js.snap
+++ b/src/components/viewPaginationRow/__tests__/__snapshots__/viewPaginationRow.test.js.snap
@@ -1,193 +1,196 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ViewPaginationRow Component should render 1`] = `
-
`;
diff --git a/src/components/viewPaginationRow/viewPaginationRow.js b/src/components/viewPaginationRow/viewPaginationRow.js
index 0d37aa7f..faf8d068 100644
--- a/src/components/viewPaginationRow/viewPaginationRow.js
+++ b/src/components/viewPaginationRow/viewPaginationRow.js
@@ -1,89 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { PaginationRow, PAGINATION_VIEW } from 'patternfly-react';
+import { Pagination, PaginationVariant } from '@patternfly/react-core';
import { reduxTypes, store } from '../../redux';
class ViewPaginationRow extends React.Component {
- onFirstPage = () => {
+ onPerPageSelect = (_e, perPage) => {
const { viewType } = this.props;
store.dispatch({
- type: reduxTypes.viewPagination.VIEW_FIRST_PAGE,
- viewType
- });
- };
-
- onLastPage = () => {
- const { viewType } = this.props;
- store.dispatch({
- type: reduxTypes.viewPagination.VIEW_LAST_PAGE,
- viewType
- });
- };
-
- onPreviousPage = () => {
- const { viewType } = this.props;
- store.dispatch({
- type: reduxTypes.viewPagination.VIEW_PREVIOUS_PAGE,
- viewType
- });
- };
-
- onNextPage = () => {
- const { viewType } = this.props;
- store.dispatch({
- type: reduxTypes.viewPagination.VIEW_NEXT_PAGE,
- viewType
- });
- };
-
- onPageInput = e => {
- const { viewType } = this.props;
- store.dispatch({
- type: reduxTypes.viewPagination.VIEW_PAGE_NUMBER,
+ type: reduxTypes.viewPagination.SET_PER_PAGE,
viewType,
- pageNumber: parseInt(e.target.value, 10)
+ pageSize: perPage
});
};
- onPerPageSelect = eventKey => {
+ onSetPage = (_e, pageNumber) => {
const { viewType } = this.props;
store.dispatch({
- type: reduxTypes.viewPagination.SET_PER_PAGE,
- viewType,
- pageSize: eventKey
+ type: reduxTypes.viewPagination.VIEW_PAGE,
+ currentPage: pageNumber,
+ viewType
});
};
render() {
- const perPageOptions = [10, 15, 25, 50, 100];
- const { currentPage, pageSize, totalCount, totalPages } = this.props;
-
- const rowPagination = {
- page: currentPage,
- perPage: pageSize,
- perPageOptions
- };
+ const { currentPage, pageSize, totalCount } = this.props;
const itemsStart = (currentPage - 1) * pageSize + 1;
const itemsEnd = Math.min(currentPage * pageSize, totalCount);
return (
-
);
}
@@ -93,16 +50,14 @@ ViewPaginationRow.propTypes = {
viewType: PropTypes.string,
currentPage: PropTypes.number,
pageSize: PropTypes.number,
- totalCount: PropTypes.number,
- totalPages: PropTypes.number
+ totalCount: PropTypes.number
};
ViewPaginationRow.defaultProps = {
viewType: null,
currentPage: 0,
pageSize: 0,
- totalCount: 0,
- totalPages: 0
+ totalCount: 0
};
export { ViewPaginationRow as default, ViewPaginationRow };
diff --git a/src/components/viewToolbar/__tests__/__snapshots__/viewToolbar.test.js.snap b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbar.test.js.snap
index 233136c4..2cbf075a 100644
--- a/src/components/viewToolbar/__tests__/__snapshots__/viewToolbar.test.js.snap
+++ b/src/components/viewToolbar/__tests__/__snapshots__/viewToolbar.test.js.snap
@@ -32,7 +32,9 @@ exports[`ViewPaginationRow Component should render 1`] = `
diff --git a/src/components/viewToolbar/viewToolbar.js b/src/components/viewToolbar/viewToolbar.js
index a54419ff..ea0e7942 100644
--- a/src/components/viewToolbar/viewToolbar.js
+++ b/src/components/viewToolbar/viewToolbar.js
@@ -178,7 +178,7 @@ class ViewToolbar extends React.Component {
currentSortType={sortType}
onSortTypeSelected={this.onUpdateCurrentSortType}
/>
-
+
+
+
+
+
+
+
+
+ 1
+
+ Lorem
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem
+
+
+
+
+ Ipsum
+
+
+
+
+ Dolor
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/components/wizard/__tests__/wizard.test.js b/src/components/wizard/__tests__/wizard.test.js
new file mode 100644
index 00000000..749f10fb
--- /dev/null
+++ b/src/components/wizard/__tests__/wizard.test.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import { ModalContent, Wizard as PfWizard } from '@patternfly/react-core';
+import { Wizard } from '../wizard';
+
+describe('Wizard Component', () => {
+ it('should render a basic component', async () => {
+ const props = {
+ isOpen: true,
+ steps: [
+ {
+ id: 1,
+ name: 'Lorem',
+ component: 'lorem'
+ },
+ {
+ id: 2,
+ name: 'Ipsum',
+ component: 'ipsum'
+ },
+ {
+ id: 3,
+ name: 'Dolor',
+ component: dolor
+ }
+ ]
+ };
+
+ const component = await mountHookComponent( );
+ expect(component.find(ModalContent).render()).toMatchSnapshot('basic');
+ });
+
+ it('should allow modifying specific and custom props', async () => {
+ const props = {
+ isOpen: true,
+ isForm: true
+ };
+
+ const formComponent = await shallowHookComponent( );
+ expect(formComponent.find(PfWizard).props().className).toMatchSnapshot('isForm');
+
+ props.isForm = false;
+ props.isNavHidden = true;
+
+ const navHiddenComponent = await shallowHookComponent( );
+ expect(navHiddenComponent.find(PfWizard).props().className).toMatchSnapshot('isNavHidden');
+ });
+});
diff --git a/src/components/wizard/wizard.js b/src/components/wizard/wizard.js
new file mode 100644
index 00000000..d47dd2d6
--- /dev/null
+++ b/src/components/wizard/wizard.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { ModalVariant, Wizard as PfWizard } from '@patternfly/react-core';
+import classNames from 'classnames';
+import { Modal } from '../modal/modal';
+
+/**
+ * FixMe: PF Wizard no longer allows "appendTo"
+ * activated here, https://github.com/patternfly/patternfly-react/pull/3102
+ * and then removed with PF-React PR#4255
+ */
+/**
+ * A PF Wizard wrapper
+ *
+ * @param {object} props
+ * @param {string|object} props.className
+ * @param {boolean} props.isForm
+ * @param {boolean} props.isNavHidden
+ * @param {boolean} props.isOpen
+ * @param {Array} props.steps
+ * @param {string} props.variant
+ * @param {object} props.props
+ * @returns {React.ReactNode}
+ */
+const Wizard = ({ className, isForm, isNavHidden, isOpen, steps, variant, ...props }) => {
+ const cssClassName = classNames(
+ 'quipucords-wizard',
+ { 'quipucords-wizard__hide-nav': isNavHidden === true },
+ { 'quipucords-wizard__hide-nav-last': isForm === true },
+ className
+ );
+
+ return (
+
+
+
+ );
+};
+
+/**
+ * Prop types
+ *
+ * @type {{isOpen: boolean, variant: string, className: string|object, isNavHidden: boolean, isForm: boolean,
+ * steps: Array}}
+ */
+Wizard.propTypes = {
+ className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ isForm: PropTypes.bool,
+ isNavHidden: PropTypes.bool,
+ isOpen: PropTypes.bool,
+ steps: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+ component: PropTypes.node.isRequired,
+ canJumpTo: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
+ enableNext: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
+ hideBackButton: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
+ hideCancelButton: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
+ name: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
+ nextButtonText: PropTypes.oneOfType([PropTypes.func, PropTypes.node])
+ })
+ ),
+ variant: PropTypes.oneOf([...Object.values(ModalVariant)])
+};
+
+/**
+ * Default props
+ *
+ * @type {{isOpen: boolean, variant: null, className: null, isNavHidden: boolean, isForm: boolean, steps: *[]}}
+ */
+Wizard.defaultProps = {
+ className: null,
+ isForm: false,
+ isNavHidden: false,
+ isOpen: false,
+ steps: [],
+ variant: null
+};
+
+export { Wizard as default, Wizard };
diff --git a/src/hooks/__tests__/__snapshots__/useTimeout.test.js.snap b/src/hooks/__tests__/__snapshots__/useTimeout.test.js.snap
new file mode 100644
index 00000000..b4dd345f
--- /dev/null
+++ b/src/hooks/__tests__/__snapshots__/useTimeout.test.js.snap
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`useTimeout should apply a hook for useTimeout: timeout 1`] = `
+Object {
+ "cancel": [Function],
+ "update": undefined,
+}
+`;
diff --git a/src/hooks/__tests__/useTimeout.test.js b/src/hooks/__tests__/useTimeout.test.js
new file mode 100644
index 00000000..ccb77225
--- /dev/null
+++ b/src/hooks/__tests__/useTimeout.test.js
@@ -0,0 +1,17 @@
+import { useTimeout } from '../useTimeout';
+
+describe('useTimeout', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ it('should apply a hook for useTimeout', async () => {
+ const mockCallback = jest.fn();
+ const mockSetTimeout = jest.spyOn(global, 'setTimeout');
+ const { result } = await mountHook(() => useTimeout(mockCallback));
+
+ expect(mockSetTimeout).toHaveBeenCalledTimes(2);
+ expect(mockCallback).toHaveBeenCalledTimes(1);
+ expect(result).toMatchSnapshot('timeout');
+ });
+});
diff --git a/src/hooks/index.js b/src/hooks/index.js
new file mode 100644
index 00000000..3bda361a
--- /dev/null
+++ b/src/hooks/index.js
@@ -0,0 +1,5 @@
+import { useTimeout } from './useTimeout';
+
+const hooks = { useTimeout };
+
+export { hooks as default, hooks, useTimeout };
diff --git a/src/hooks/useTimeout.js b/src/hooks/useTimeout.js
new file mode 100644
index 00000000..ba84ebae
--- /dev/null
+++ b/src/hooks/useTimeout.js
@@ -0,0 +1,29 @@
+import { useEffect, useRef, useState } from 'react';
+import { helpers } from '../common';
+
+/**
+ * window.setTimeout hook polling hook with multiple cancel alternatives
+ *
+ * @param {function(): boolean} callback Callback can return a boolean, with false cancelling the timeout.
+ * @param {number} pollInterval
+ * @returns {{cancel: function(): void, update: number|undefined}} A cancel function is returned, calling it cancels the setTimeout. The returned "update" value is a time increment, always adding up, and purposefully causing a hook update.
+ */
+const useTimeout = (callback, pollInterval = 0) => {
+ const timer = useRef();
+ const [update, setUpdate] = useState();
+ const result = callback();
+
+ useEffect(() => {
+ if (result !== false) {
+ timer.current = window.setTimeout(() => setUpdate(helpers.getCurrentDate().getTime()), pollInterval);
+ }
+
+ return () => {
+ window.clearTimeout(timer.current);
+ };
+ }, [pollInterval, update, result]);
+
+ return { update, cancel: () => window.clearTimeout(timer.current) };
+};
+
+export { useTimeout as default, useTimeout };
diff --git a/src/index.js b/src/index.js
index edf7aee1..f1846db0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,8 +5,6 @@ import { BrowserRouter } from 'react-router-dom';
import App from './components/app';
import { baseName } from './components/router/router';
import { store } from './redux/store';
-import 'patternfly/dist/css/rcue.css';
-import 'patternfly/dist/css/rcue-additions.css';
import './styles/index.scss';
ReactDOM.render(
diff --git a/src/redux/actions/__tests__/scansActions.test.js b/src/redux/actions/__tests__/scansActions.test.js
index 78b63b6e..a7f47153 100644
--- a/src/redux/actions/__tests__/scansActions.test.js
+++ b/src/redux/actions/__tests__/scansActions.test.js
@@ -122,7 +122,7 @@ describe('ScansActions', () => {
dispatcher(store.dispatch).then(() => {
const response = store.getState().scans;
- expect(response.start.lorem.fulfilled).toEqual(true);
+ expect(response.action.lorem.fulfilled).toEqual(true);
done();
});
});
@@ -134,7 +134,7 @@ describe('ScansActions', () => {
dispatcher(store.dispatch).then(() => {
const response = store.getState().scans;
- expect(response.pause.lorem.fulfilled).toEqual(true);
+ expect(response.action.lorem.fulfilled).toEqual(true);
done();
});
});
@@ -146,7 +146,7 @@ describe('ScansActions', () => {
dispatcher(store.dispatch).then(() => {
const response = store.getState().scans;
- expect(response.cancel.lorem.fulfilled).toEqual(true);
+ expect(response.action.lorem.fulfilled).toEqual(true);
done();
});
});
@@ -158,7 +158,7 @@ describe('ScansActions', () => {
dispatcher(store.dispatch).then(() => {
const response = store.getState().scans;
- expect(response.restart.lorem.fulfilled).toEqual(true);
+ expect(response.action.lorem.fulfilled).toEqual(true);
done();
});
});
diff --git a/src/redux/common/reduxHelpers.js b/src/redux/common/reduxHelpers.js
index 95e384e6..f04c34e6 100644
--- a/src/redux/common/reduxHelpers.js
+++ b/src/redux/common/reduxHelpers.js
@@ -1,3 +1,4 @@
+import _get from 'lodash/get';
import { helpers } from '../../common/helpers';
const FULFILLED_ACTION = base => `${base}_FULFILLED`;
@@ -76,8 +77,17 @@ const generatedPromiseActionReducer = (types = [], state = {}, action = {}) => {
update: false
};
- const idUse = data =>
- (action.meta && action.meta.id && { [action.meta.id]: { ...baseState, ...data } }) || { ...baseState, ...data };
+ // Automatically apply data and state to a contextual ID if meta.id exists.
+ const idUse = data => {
+ const typeId = typeof action?.meta?.id;
+ return (
+ ((typeId === 'string' || typeId === 'number') &&
+ action?.meta?.id && { [action.meta.id]: { ...baseState, ...data } }) || {
+ ...baseState,
+ ...data
+ }
+ );
+ };
switch (type) {
case REJECTED_ACTION(whichType.type || whichType):
@@ -107,7 +117,11 @@ const generatedPromiseActionReducer = (types = [], state = {}, action = {}) => {
return setStateProp(
whichType.ref || null,
idUse({
- date: action.payload.headers && action.payload.headers.date,
+ date: _get(
+ action.payload,
+ 'headers.date',
+ (helpers.DEV_MODE && helpers.getCurrentDate().toUTCString()) || undefined
+ ),
data: (action.payload && action.payload.data) || {},
fulfilled: true
}),
diff --git a/src/redux/constants/__tests__/__snapshots__/index.test.js.snap b/src/redux/constants/__tests__/__snapshots__/index.test.js.snap
index 553766ca..fb874b78 100644
--- a/src/redux/constants/__tests__/__snapshots__/index.test.js.snap
+++ b/src/redux/constants/__tests__/__snapshots__/index.test.js.snap
@@ -57,8 +57,10 @@ 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",
@@ -66,20 +68,75 @@ Object {
"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",
+ "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",
+ "CONFIRM_DELETE_SOURCE": "CONFIRM_DELETE_SOURCE",
"CREATE_SOURCE_SHOW": "CREATE_SOURCE_SHOW",
"DELETE_SOURCE": "DELETE_SOURCE",
"DELETE_SOURCES": "DELETE_SOURCES",
+ "DESELECT_SOURCE": "DESELECT_SOURCE",
"EDIT_SOURCE_SHOW": "EDIT_SOURCE_SHOW",
+ "EXPANDED_SOURCE": "EXPANDED_SOURCE",
"GET_SCANS_SOURCES": "GET_SCANS_SOURCES",
"GET_SOURCE": "GET_SOURCE",
"GET_SOURCES": "GET_SOURCES",
+ "NOT_EXPANDED_SOURCE": "NOT_EXPANDED_SOURCE",
+ "RESET_DELETE_SOURCE": "RESET_DELETE_SOURCE",
+ "SELECT_SOURCE": "SELECT_SOURCE",
"UPDATE_SOURCE": "UPDATE_SOURCE",
"UPDATE_SOURCES": "UPDATE_SOURCES",
"UPDATE_SOURCE_HIDE": "UPDATE_SOURCE_HIDE",
@@ -113,11 +170,7 @@ Object {
},
"viewPagination": Object {
"SET_PER_PAGE": "SET_PER_PAGE",
- "VIEW_FIRST_PAGE": "VIEW_FIRST_PAGE",
- "VIEW_LAST_PAGE": "VIEW_LAST_PAGE",
- "VIEW_NEXT_PAGE": "VIEW_NEXT_PAGE",
- "VIEW_PAGE_NUMBER": "VIEW_PAGE_NUMBER",
- "VIEW_PREVIOUS_PAGE": "VIEW_PREVIOUS_PAGE",
+ "VIEW_PAGE": "VIEW_PAGE",
},
"viewToolbar": Object {
"ADD_FILTER": "ADD_FILTER",
@@ -167,8 +220,10 @@ 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",
@@ -176,20 +231,75 @@ Object {
"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",
+ "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",
+ "CONFIRM_DELETE_SOURCE": "CONFIRM_DELETE_SOURCE",
"CREATE_SOURCE_SHOW": "CREATE_SOURCE_SHOW",
"DELETE_SOURCE": "DELETE_SOURCE",
"DELETE_SOURCES": "DELETE_SOURCES",
+ "DESELECT_SOURCE": "DESELECT_SOURCE",
"EDIT_SOURCE_SHOW": "EDIT_SOURCE_SHOW",
+ "EXPANDED_SOURCE": "EXPANDED_SOURCE",
"GET_SCANS_SOURCES": "GET_SCANS_SOURCES",
"GET_SOURCE": "GET_SOURCE",
"GET_SOURCES": "GET_SOURCES",
+ "NOT_EXPANDED_SOURCE": "NOT_EXPANDED_SOURCE",
+ "RESET_DELETE_SOURCE": "RESET_DELETE_SOURCE",
+ "SELECT_SOURCE": "SELECT_SOURCE",
"UPDATE_SOURCE": "UPDATE_SOURCE",
"UPDATE_SOURCES": "UPDATE_SOURCES",
"UPDATE_SOURCE_HIDE": "UPDATE_SOURCE_HIDE",
@@ -223,11 +333,7 @@ Object {
},
"viewPagination": Object {
"SET_PER_PAGE": "SET_PER_PAGE",
- "VIEW_FIRST_PAGE": "VIEW_FIRST_PAGE",
- "VIEW_LAST_PAGE": "VIEW_LAST_PAGE",
- "VIEW_NEXT_PAGE": "VIEW_NEXT_PAGE",
- "VIEW_PAGE_NUMBER": "VIEW_PAGE_NUMBER",
- "VIEW_PREVIOUS_PAGE": "VIEW_PREVIOUS_PAGE",
+ "VIEW_PAGE": "VIEW_PAGE",
},
"viewToolbar": Object {
"ADD_FILTER": "ADD_FILTER",
@@ -247,8 +353,10 @@ 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",
@@ -256,20 +364,75 @@ Object {
"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",
+ "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",
+ "CONFIRM_DELETE_SOURCE": "CONFIRM_DELETE_SOURCE",
"CREATE_SOURCE_SHOW": "CREATE_SOURCE_SHOW",
"DELETE_SOURCE": "DELETE_SOURCE",
"DELETE_SOURCES": "DELETE_SOURCES",
+ "DESELECT_SOURCE": "DESELECT_SOURCE",
"EDIT_SOURCE_SHOW": "EDIT_SOURCE_SHOW",
+ "EXPANDED_SOURCE": "EXPANDED_SOURCE",
"GET_SCANS_SOURCES": "GET_SCANS_SOURCES",
"GET_SOURCE": "GET_SOURCE",
"GET_SOURCES": "GET_SOURCES",
+ "NOT_EXPANDED_SOURCE": "NOT_EXPANDED_SOURCE",
+ "RESET_DELETE_SOURCE": "RESET_DELETE_SOURCE",
+ "SELECT_SOURCE": "SELECT_SOURCE",
"UPDATE_SOURCE": "UPDATE_SOURCE",
"UPDATE_SOURCES": "UPDATE_SOURCES",
"UPDATE_SOURCE_HIDE": "UPDATE_SOURCE_HIDE",
@@ -295,11 +458,7 @@ Object {
},
"viewPaginationTypes": Object {
"SET_PER_PAGE": "SET_PER_PAGE",
- "VIEW_FIRST_PAGE": "VIEW_FIRST_PAGE",
- "VIEW_LAST_PAGE": "VIEW_LAST_PAGE",
- "VIEW_NEXT_PAGE": "VIEW_NEXT_PAGE",
- "VIEW_PAGE_NUMBER": "VIEW_PAGE_NUMBER",
- "VIEW_PREVIOUS_PAGE": "VIEW_PREVIOUS_PAGE",
+ "VIEW_PAGE": "VIEW_PAGE",
},
"viewToolbarTypes": Object {
"ADD_FILTER": "ADD_FILTER",
@@ -356,8 +515,10 @@ 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",
@@ -365,20 +526,75 @@ Object {
"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",
+ "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",
+ "CONFIRM_DELETE_SOURCE": "CONFIRM_DELETE_SOURCE",
"CREATE_SOURCE_SHOW": "CREATE_SOURCE_SHOW",
"DELETE_SOURCE": "DELETE_SOURCE",
"DELETE_SOURCES": "DELETE_SOURCES",
+ "DESELECT_SOURCE": "DESELECT_SOURCE",
"EDIT_SOURCE_SHOW": "EDIT_SOURCE_SHOW",
+ "EXPANDED_SOURCE": "EXPANDED_SOURCE",
"GET_SCANS_SOURCES": "GET_SCANS_SOURCES",
"GET_SOURCE": "GET_SOURCE",
"GET_SOURCES": "GET_SOURCES",
+ "NOT_EXPANDED_SOURCE": "NOT_EXPANDED_SOURCE",
+ "RESET_DELETE_SOURCE": "RESET_DELETE_SOURCE",
+ "SELECT_SOURCE": "SELECT_SOURCE",
"UPDATE_SOURCE": "UPDATE_SOURCE",
"UPDATE_SOURCES": "UPDATE_SOURCES",
"UPDATE_SOURCE_HIDE": "UPDATE_SOURCE_HIDE",
@@ -412,11 +628,7 @@ Object {
},
"viewPagination": Object {
"SET_PER_PAGE": "SET_PER_PAGE",
- "VIEW_FIRST_PAGE": "VIEW_FIRST_PAGE",
- "VIEW_LAST_PAGE": "VIEW_LAST_PAGE",
- "VIEW_NEXT_PAGE": "VIEW_NEXT_PAGE",
- "VIEW_PAGE_NUMBER": "VIEW_PAGE_NUMBER",
- "VIEW_PREVIOUS_PAGE": "VIEW_PREVIOUS_PAGE",
+ "VIEW_PAGE": "VIEW_PAGE",
},
"viewToolbar": Object {
"ADD_FILTER": "ADD_FILTER",
diff --git a/src/redux/constants/index.js b/src/redux/constants/index.js
index 30ffb585..f553b246 100644
--- a/src/redux/constants/index.js
+++ b/src/redux/constants/index.js
@@ -4,12 +4,12 @@ import * as credentialsTypes from './credentialsConstants';
import * as factsTypes from './factsConstants';
import * as reportsTypes from './reportsConstants';
import * as scansTypes from './scansConstants';
-import * as sourcesTypes from './sourcesConstants';
+import { sourcesTypes } from './sourcesConstants';
import * as statusTypes from './statusConstants';
import * as toastNotificationTypes from './toasNotificationConstants';
import * as userTypes from './userConstants';
import * as viewTypes from './viewConstants';
-import * as viewPaginationTypes from './viewPaginationConstants';
+import { viewPaginationTypes } from './viewPaginationConstants';
import * as viewToolbarTypes from './viewToolbarConstants';
const reduxTypes = {
@@ -29,6 +29,7 @@ const reduxTypes = {
};
export {
+ reduxTypes as default,
reduxTypes,
aboutModalTypes,
confirmationModalTypes,
@@ -44,5 +45,3 @@ export {
viewPaginationTypes,
viewToolbarTypes
};
-
-export default reduxTypes;
diff --git a/src/redux/constants/scansConstants.js b/src/redux/constants/scansConstants.js
index 25dec6ab..440d9e82 100644
--- a/src/redux/constants/scansConstants.js
+++ b/src/redux/constants/scansConstants.js
@@ -18,7 +18,39 @@ const EDIT_SCAN_SHOW = 'EDIT_SCAN_SHOW';
const MERGE_SCAN_DIALOG_HIDE = 'MERGE_SCAN_DIALOG_HIDE';
const MERGE_SCAN_DIALOG_SHOW = 'MERGE_SCAN_DIALOG_SHOW';
+const SELECT_SCAN = 'SELECT_SCAN';
+const DESELECT_SCAN = 'DESELECT_SCAN';
+const EXPANDED_SCAN = 'EXPANDED_SCAN';
+const NOT_EXPANDED_SCAN = 'NOT_EXPANDED_SCAN';
+const RESET_DELETE_SCAN = 'RESET_DELETE_SCAN';
+
+const scansTypes = {
+ ADD_SCAN,
+ ADD_START_SCAN,
+ UPDATE_SCANS,
+ GET_SCANS,
+ GET_SCAN_CONNECTION_RESULTS,
+ GET_SCAN_INSPECTION_RESULTS,
+ GET_SCAN_JOB,
+ GET_SCAN_JOBS,
+ START_SCAN,
+ CANCEL_SCAN,
+ PAUSE_SCAN,
+ RESTART_SCAN,
+ EDIT_SCAN_HIDE,
+ EDIT_SCAN_SHOW,
+ MERGE_SCAN_DIALOG_HIDE,
+ MERGE_SCAN_DIALOG_SHOW,
+ SELECT_SCAN,
+ DESELECT_SCAN,
+ EXPANDED_SCAN,
+ NOT_EXPANDED_SCAN,
+ RESET_DELETE_SCAN
+};
+
export {
+ scansTypes as default,
+ scansTypes,
ADD_SCAN,
ADD_START_SCAN,
UPDATE_SCANS,
@@ -34,5 +66,10 @@ export {
EDIT_SCAN_HIDE,
EDIT_SCAN_SHOW,
MERGE_SCAN_DIALOG_HIDE,
- MERGE_SCAN_DIALOG_SHOW
+ MERGE_SCAN_DIALOG_SHOW,
+ SELECT_SCAN,
+ DESELECT_SCAN,
+ EXPANDED_SCAN,
+ NOT_EXPANDED_SCAN,
+ RESET_DELETE_SCAN
};
diff --git a/src/redux/constants/sourcesConstants.js b/src/redux/constants/sourcesConstants.js
index 4a321714..fbe5f594 100644
--- a/src/redux/constants/sourcesConstants.js
+++ b/src/redux/constants/sourcesConstants.js
@@ -1,4 +1,5 @@
const ADD_SOURCE = 'ADD_SOURCE';
+const CONFIRM_DELETE_SOURCE = 'CONFIRM_DELETE_SOURCE';
const DELETE_SOURCE = 'DELETE_SOURCE';
const DELETE_SOURCES = 'DELETE_SOURCES';
const GET_SCANS_SOURCES = 'GET_SCANS_SOURCES';
@@ -12,9 +13,40 @@ const VALID_SOURCE_WIZARD_STEPTWO = 'VALID_SOURCE_WIZARD_STEPTWO';
const CREATE_SOURCE_SHOW = 'CREATE_SOURCE_SHOW';
const EDIT_SOURCE_SHOW = 'EDIT_SOURCE_SHOW';
const UPDATE_SOURCE_HIDE = 'UPDATE_SOURCE_HIDE';
+const SELECT_SOURCE = 'SELECT_SOURCE';
+const DESELECT_SOURCE = 'DESELECT_SOURCE';
+const EXPANDED_SOURCE = 'EXPANDED_SOURCE';
+const NOT_EXPANDED_SOURCE = 'NOT_EXPANDED_SOURCE';
+const RESET_DELETE_SOURCE = 'RESET_DELETE_SOURCE';
+
+const sourcesTypes = {
+ ADD_SOURCE,
+ CONFIRM_DELETE_SOURCE,
+ DELETE_SOURCE,
+ DELETE_SOURCES,
+ GET_SCANS_SOURCES,
+ GET_SOURCE,
+ GET_SOURCES,
+ UPDATE_SOURCES,
+ UPDATE_SOURCE,
+ UPDATE_SOURCE_WIZARD,
+ VALID_SOURCE_WIZARD_STEPONE,
+ VALID_SOURCE_WIZARD_STEPTWO,
+ CREATE_SOURCE_SHOW,
+ EDIT_SOURCE_SHOW,
+ UPDATE_SOURCE_HIDE,
+ SELECT_SOURCE,
+ DESELECT_SOURCE,
+ EXPANDED_SOURCE,
+ NOT_EXPANDED_SOURCE,
+ RESET_DELETE_SOURCE
+};
export {
+ sourcesTypes as default,
+ sourcesTypes,
ADD_SOURCE,
+ CONFIRM_DELETE_SOURCE,
DELETE_SOURCE,
DELETE_SOURCES,
GET_SCANS_SOURCES,
@@ -27,5 +59,10 @@ export {
VALID_SOURCE_WIZARD_STEPTWO,
CREATE_SOURCE_SHOW,
EDIT_SOURCE_SHOW,
- UPDATE_SOURCE_HIDE
+ UPDATE_SOURCE_HIDE,
+ SELECT_SOURCE,
+ DESELECT_SOURCE,
+ EXPANDED_SOURCE,
+ NOT_EXPANDED_SOURCE,
+ RESET_DELETE_SOURCE
};
diff --git a/src/redux/constants/viewPaginationConstants.js b/src/redux/constants/viewPaginationConstants.js
index 8c20d629..ce1bd0df 100644
--- a/src/redux/constants/viewPaginationConstants.js
+++ b/src/redux/constants/viewPaginationConstants.js
@@ -1,8 +1,9 @@
-const VIEW_FIRST_PAGE = 'VIEW_FIRST_PAGE';
-const VIEW_LAST_PAGE = 'VIEW_LAST_PAGE';
-const VIEW_PREVIOUS_PAGE = 'VIEW_PREVIOUS_PAGE';
-const VIEW_NEXT_PAGE = 'VIEW_NEXT_PAGE';
-const VIEW_PAGE_NUMBER = 'VIEW_PAGE_NUMBER';
+const VIEW_PAGE = 'VIEW_PAGE';
const SET_PER_PAGE = 'SET_PER_PAGE';
-export { VIEW_FIRST_PAGE, VIEW_LAST_PAGE, VIEW_PREVIOUS_PAGE, VIEW_NEXT_PAGE, VIEW_PAGE_NUMBER, SET_PER_PAGE };
+const viewPaginationTypes = {
+ SET_PER_PAGE,
+ VIEW_PAGE
+};
+
+export { viewPaginationTypes as default, viewPaginationTypes, SET_PER_PAGE, VIEW_PAGE };
diff --git a/src/redux/index.js b/src/redux/index.js
index c429c003..ba2af820 100644
--- a/src/redux/index.js
+++ b/src/redux/index.js
@@ -1,6 +1,5 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
-import { withTranslation } from 'react-i18next';
import { store } from './store';
import { reduxActions } from './actions';
import { reduxHelpers } from './common';
@@ -8,13 +7,6 @@ import { storeHooks } from './hooks';
import { reduxReducers } from './reducers';
import { reduxSelectors } from './selectors';
import { reduxTypes } from './constants';
-import { helpers } from '../common/helpers';
-
-const connectTranslate = (mapStateToProps, mapDispatchToProps) => component =>
- connect(mapStateToProps, mapDispatchToProps)((!helpers.TEST_MODE && withTranslation()(component)) || component);
-
-const connectRouterTranslate = (mapStateToProps, mapDispatchToProps) => component =>
- withRouter(connectTranslate(mapStateToProps, mapDispatchToProps)(component));
const connectRouter = (mapStateToProps, mapDispatchToProps) => component =>
withRouter(connect(mapStateToProps, mapDispatchToProps)(component));
@@ -22,8 +14,6 @@ const connectRouter = (mapStateToProps, mapDispatchToProps) => component =>
export {
connect,
connectRouter,
- connectRouterTranslate,
- connectTranslate,
reduxActions,
reduxHelpers,
reduxReducers,
diff --git a/src/redux/reducers/__tests__/__snapshots__/addSourceWizardReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/addSourceWizardReducer.test.js.snap
index 7240668f..49223a45 100644
--- a/src/redux/reducers/__tests__/__snapshots__/addSourceWizardReducer.test.js.snap
+++ b/src/redux/reducers/__tests__/__snapshots__/addSourceWizardReducer.test.js.snap
@@ -53,7 +53,7 @@ Object {
"errorStatus": null,
"fulfilled": true,
"pending": false,
- "show": true,
+ "show": false,
"source": Object {
"test": "success",
},
@@ -97,7 +97,7 @@ Object {
"errorStatus": null,
"fulfilled": true,
"pending": false,
- "show": true,
+ "show": false,
"source": Object {
"test": "success",
},
diff --git a/src/redux/reducers/__tests__/__snapshots__/confirmationModalReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/confirmationModalReducer.test.js.snap
index dd2961de..43c07330 100644
--- a/src/redux/reducers/__tests__/__snapshots__/confirmationModalReducer.test.js.snap
+++ b/src/redux/reducers/__tests__/__snapshots__/confirmationModalReducer.test.js.snap
@@ -27,6 +27,7 @@ Object {
"onConfirm": undefined,
"show": true,
"title": undefined,
+ "variant": undefined,
},
"type": "CONFIRMATION_MODAL_SHOW",
}
diff --git a/src/redux/reducers/__tests__/__snapshots__/scansReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/scansReducer.test.js.snap
index 69b1241d..0d821fe5 100644
--- a/src/redux/reducers/__tests__/__snapshots__/scansReducer.test.js.snap
+++ b/src/redux/reducers/__tests__/__snapshots__/scansReducer.test.js.snap
@@ -3,7 +3,7 @@
exports[`ScansReducer should handle all defined error types: rejected types CANCEL_SCAN 1`] = `
Object {
"result": Object {
- "cancel": Object {
+ "action": Object {
"error": true,
"errorMessage": "ERROR",
"errorStatus": 0,
@@ -16,6 +16,7 @@ Object {
},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -24,9 +25,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "CANCEL_SCAN_REJECTED",
@@ -36,7 +36,7 @@ Object {
exports[`ScansReducer should handle all defined error types: rejected types GET_SCAN_CONNECTION_RESULTS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {
"error": true,
"errorMessage": "ERROR",
@@ -49,6 +49,7 @@ Object {
"update": false,
},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -57,9 +58,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_CONNECTION_RESULTS_REJECTED",
@@ -69,9 +69,10 @@ Object {
exports[`ScansReducer should handle all defined error types: rejected types GET_SCAN_INSPECTION_RESULTS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {
"error": true,
"errorMessage": "ERROR",
@@ -90,9 +91,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_INSPECTION_RESULTS_REJECTED",
@@ -102,9 +102,10 @@ Object {
exports[`ScansReducer should handle all defined error types: rejected types GET_SCAN_JOB 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {
"error": true,
@@ -123,9 +124,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_JOB_REJECTED",
@@ -135,9 +135,10 @@ Object {
exports[`ScansReducer should handle all defined error types: rejected types GET_SCAN_JOBS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {
@@ -156,9 +157,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_JOBS_REJECTED",
@@ -168,9 +168,10 @@ Object {
exports[`ScansReducer should handle all defined error types: rejected types GET_SCANS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -179,9 +180,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {
"error": true,
"errorMessage": "ERROR",
@@ -201,7 +201,7 @@ Object {
exports[`ScansReducer should handle all defined error types: rejected types GET_SCANS_SOURCES 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {
"error": true,
@@ -214,6 +214,7 @@ Object {
"pending": false,
"update": false,
},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -222,9 +223,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCANS_SOURCES_REJECTED",
@@ -234,18 +234,7 @@ Object {
exports[`ScansReducer should handle all defined error types: rejected types PAUSE_SCAN 1`] = `
Object {
"result": Object {
- "cancel": Object {},
- "connection": Object {},
- "empty": Object {},
- "inspection": Object {},
- "job": Object {},
- "jobs": Object {},
- "mergeDialog": Object {
- "details": false,
- "scans": Array [],
- "show": false,
- },
- "pause": Object {
+ "action": Object {
"error": true,
"errorMessage": "ERROR",
"errorStatus": 0,
@@ -256,20 +245,9 @@ Object {
"pending": false,
"update": false,
},
- "restart": Object {},
- "start": Object {},
- "view": Object {},
- },
- "type": "PAUSE_SCAN_REJECTED",
-}
-`;
-
-exports[`ScansReducer should handle all defined error types: rejected types RESTART_SCAN 1`] = `
-Object {
- "result": Object {
- "cancel": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -278,8 +256,18 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "PAUSE_SCAN_REJECTED",
+}
+`;
+
+exports[`ScansReducer should handle all defined error types: rejected types RESTART_SCAN 1`] = `
+Object {
+ "result": Object {
+ "action": Object {
"error": true,
"errorMessage": "ERROR",
"errorStatus": 0,
@@ -290,19 +278,9 @@ Object {
"pending": false,
"update": false,
},
- "start": Object {},
- "view": Object {},
- },
- "type": "RESTART_SCAN_REJECTED",
-}
-`;
-
-exports[`ScansReducer should handle all defined error types: rejected types START_SCAN 1`] = `
-Object {
- "result": Object {
- "cancel": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -311,9 +289,18 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "RESTART_SCAN_REJECTED",
+}
+`;
+
+exports[`ScansReducer should handle all defined error types: rejected types START_SCAN 1`] = `
+Object {
+ "result": Object {
+ "action": Object {
"error": true,
"errorMessage": "ERROR",
"errorStatus": 0,
@@ -324,6 +311,19 @@ Object {
"pending": false,
"update": false,
},
+ "connection": Object {},
+ "empty": Object {},
+ "expanded": Object {},
+ "inspection": Object {},
+ "job": Object {},
+ "jobs": Object {},
+ "mergeDialog": Object {
+ "details": false,
+ "scans": Array [],
+ "show": false,
+ },
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "START_SCAN_REJECTED",
@@ -333,7 +333,7 @@ Object {
exports[`ScansReducer should handle all defined fulfilled types: fulfilled types CANCEL_SCAN 1`] = `
Object {
"result": Object {
- "cancel": Object {
+ "action": Object {
"data": Object {
"test": "success",
},
@@ -349,6 +349,7 @@ Object {
},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -357,9 +358,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "CANCEL_SCAN_FULFILLED",
@@ -369,7 +369,7 @@ Object {
exports[`ScansReducer should handle all defined fulfilled types: fulfilled types GET_SCAN_CONNECTION_RESULTS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {
"data": Object {
"test": "success",
@@ -385,6 +385,7 @@ Object {
"update": false,
},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -393,9 +394,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_CONNECTION_RESULTS_FULFILLED",
@@ -405,9 +405,10 @@ Object {
exports[`ScansReducer should handle all defined fulfilled types: fulfilled types GET_SCAN_INSPECTION_RESULTS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {
"data": Object {
"test": "success",
@@ -429,9 +430,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_INSPECTION_RESULTS_FULFILLED",
@@ -441,9 +441,10 @@ Object {
exports[`ScansReducer should handle all defined fulfilled types: fulfilled types GET_SCAN_JOB 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {
"data": Object {
@@ -465,9 +466,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_JOB_FULFILLED",
@@ -477,9 +477,10 @@ Object {
exports[`ScansReducer should handle all defined fulfilled types: fulfilled types GET_SCAN_JOBS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {
@@ -501,9 +502,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_JOBS_FULFILLED",
@@ -513,9 +513,10 @@ Object {
exports[`ScansReducer should handle all defined fulfilled types: fulfilled types GET_SCANS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -524,9 +525,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {
"data": Object {
"test": "success",
@@ -549,7 +549,7 @@ Object {
exports[`ScansReducer should handle all defined fulfilled types: fulfilled types GET_SCANS_SOURCES 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {
"data": Object {
@@ -565,6 +565,7 @@ Object {
"pending": false,
"update": false,
},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -573,9 +574,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCANS_SOURCES_FULFILLED",
@@ -585,18 +585,7 @@ Object {
exports[`ScansReducer should handle all defined fulfilled types: fulfilled types PAUSE_SCAN 1`] = `
Object {
"result": Object {
- "cancel": Object {},
- "connection": Object {},
- "empty": Object {},
- "inspection": Object {},
- "job": Object {},
- "jobs": Object {},
- "mergeDialog": Object {
- "details": false,
- "scans": Array [],
- "show": false,
- },
- "pause": Object {
+ "action": Object {
"data": Object {
"test": "success",
},
@@ -610,20 +599,9 @@ Object {
"pending": false,
"update": false,
},
- "restart": Object {},
- "start": Object {},
- "view": Object {},
- },
- "type": "PAUSE_SCAN_FULFILLED",
-}
-`;
-
-exports[`ScansReducer should handle all defined fulfilled types: fulfilled types RESTART_SCAN 1`] = `
-Object {
- "result": Object {
- "cancel": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -632,8 +610,18 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "PAUSE_SCAN_FULFILLED",
+}
+`;
+
+exports[`ScansReducer should handle all defined fulfilled types: fulfilled types RESTART_SCAN 1`] = `
+Object {
+ "result": Object {
+ "action": Object {
"data": Object {
"test": "success",
},
@@ -647,19 +635,9 @@ Object {
"pending": false,
"update": false,
},
- "start": Object {},
- "view": Object {},
- },
- "type": "RESTART_SCAN_FULFILLED",
-}
-`;
-
-exports[`ScansReducer should handle all defined fulfilled types: fulfilled types START_SCAN 1`] = `
-Object {
- "result": Object {
- "cancel": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -668,9 +646,18 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "RESTART_SCAN_FULFILLED",
+}
+`;
+
+exports[`ScansReducer should handle all defined fulfilled types: fulfilled types START_SCAN 1`] = `
+Object {
+ "result": Object {
+ "action": Object {
"data": Object {
"test": "success",
},
@@ -684,6 +671,19 @@ Object {
"pending": false,
"update": false,
},
+ "connection": Object {},
+ "empty": Object {},
+ "expanded": Object {},
+ "inspection": Object {},
+ "job": Object {},
+ "jobs": Object {},
+ "mergeDialog": Object {
+ "details": false,
+ "scans": Array [],
+ "show": false,
+ },
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "START_SCAN_FULFILLED",
@@ -693,7 +693,7 @@ Object {
exports[`ScansReducer should handle all defined pending types: pending types CANCEL_SCAN 1`] = `
Object {
"result": Object {
- "cancel": Object {
+ "action": Object {
"error": false,
"errorMessage": "",
"fulfilled": false,
@@ -705,6 +705,7 @@ Object {
},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -713,9 +714,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "CANCEL_SCAN_PENDING",
@@ -725,7 +725,7 @@ Object {
exports[`ScansReducer should handle all defined pending types: pending types GET_SCAN_CONNECTION_RESULTS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {
"error": false,
"errorMessage": "",
@@ -737,6 +737,7 @@ Object {
"update": false,
},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -745,9 +746,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_CONNECTION_RESULTS_PENDING",
@@ -757,9 +757,10 @@ Object {
exports[`ScansReducer should handle all defined pending types: pending types GET_SCAN_INSPECTION_RESULTS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {
"error": false,
"errorMessage": "",
@@ -777,9 +778,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_INSPECTION_RESULTS_PENDING",
@@ -789,9 +789,10 @@ Object {
exports[`ScansReducer should handle all defined pending types: pending types GET_SCAN_JOB 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {
"error": false,
@@ -809,9 +810,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_JOB_PENDING",
@@ -821,9 +821,10 @@ Object {
exports[`ScansReducer should handle all defined pending types: pending types GET_SCAN_JOBS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {
@@ -841,9 +842,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCAN_JOBS_PENDING",
@@ -853,9 +853,10 @@ Object {
exports[`ScansReducer should handle all defined pending types: pending types GET_SCANS 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -864,9 +865,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {
"error": false,
"errorMessage": "",
@@ -885,7 +885,7 @@ Object {
exports[`ScansReducer should handle all defined pending types: pending types GET_SCANS_SOURCES 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {
"error": false,
@@ -897,6 +897,7 @@ Object {
"pending": true,
"update": false,
},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -905,9 +906,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "GET_SCANS_SOURCES_PENDING",
@@ -917,9 +917,19 @@ Object {
exports[`ScansReducer should handle all defined pending types: pending types PAUSE_SCAN 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {
+ "error": false,
+ "errorMessage": "",
+ "fulfilled": false,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
+ "pending": true,
+ "update": false,
+ },
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -928,18 +938,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {
- "error": false,
- "errorMessage": "",
- "fulfilled": false,
- "metaData": undefined,
- "metaId": undefined,
- "metaQuery": undefined,
- "pending": true,
- "update": false,
- },
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "PAUSE_SCAN_PENDING",
@@ -949,9 +949,19 @@ Object {
exports[`ScansReducer should handle all defined pending types: pending types RESTART_SCAN 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {
+ "error": false,
+ "errorMessage": "",
+ "fulfilled": false,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
+ "pending": true,
+ "update": false,
+ },
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -960,8 +970,18 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "RESTART_SCAN_PENDING",
+}
+`;
+
+exports[`ScansReducer should handle all defined pending types: pending types START_SCAN 1`] = `
+Object {
+ "result": Object {
+ "action": Object {
"error": false,
"errorMessage": "",
"fulfilled": false,
@@ -971,19 +991,32 @@ Object {
"pending": true,
"update": false,
},
- "start": Object {},
+ "connection": Object {},
+ "empty": Object {},
+ "expanded": Object {},
+ "inspection": Object {},
+ "job": Object {},
+ "jobs": Object {},
+ "mergeDialog": Object {
+ "details": false,
+ "scans": Array [],
+ "show": false,
+ },
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
- "type": "RESTART_SCAN_PENDING",
+ "type": "START_SCAN_PENDING",
}
`;
-exports[`ScansReducer should handle all defined pending types: pending types START_SCAN 1`] = `
+exports[`ScansReducer should handle specific defined types: defined type DESELECT_SCAN 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -992,30 +1025,48 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {
- "error": false,
- "errorMessage": "",
- "fulfilled": false,
- "metaData": undefined,
- "metaId": undefined,
- "metaQuery": undefined,
- "pending": true,
- "update": false,
+ "selected": Object {
+ "undefined": null,
},
+ "update": 0,
"view": Object {},
},
- "type": "START_SCAN_PENDING",
+ "type": "DESELECT_SCAN",
+}
+`;
+
+exports[`ScansReducer should handle specific defined types: defined type EXPANDED_SCAN 1`] = `
+Object {
+ "result": Object {
+ "action": Object {},
+ "connection": Object {},
+ "empty": Object {},
+ "expanded": Object {
+ "undefined": undefined,
+ },
+ "inspection": Object {},
+ "job": Object {},
+ "jobs": Object {},
+ "mergeDialog": Object {
+ "details": false,
+ "scans": Array [],
+ "show": false,
+ },
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "EXPANDED_SCAN",
}
`;
exports[`ScansReducer should handle specific defined types: defined type MERGE_SCAN_DIALOG_HIDE 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -1024,9 +1075,8 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "MERGE_SCAN_DIALOG_HIDE",
@@ -1036,9 +1086,10 @@ Object {
exports[`ScansReducer should handle specific defined types: defined type MERGE_SCAN_DIALOG_SHOW 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {},
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -1047,21 +1098,23 @@ Object {
"scans": undefined,
"show": true,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {},
},
"type": "MERGE_SCAN_DIALOG_SHOW",
}
`;
-exports[`ScansReducer should handle specific defined types: defined type UPDATE_SCANS 1`] = `
+exports[`ScansReducer should handle specific defined types: defined type NOT_EXPANDED_SCAN 1`] = `
Object {
"result": Object {
- "cancel": Object {},
+ "action": Object {},
"connection": Object {},
"empty": Object {},
+ "expanded": Object {
+ "undefined": null,
+ },
"inspection": Object {},
"job": Object {},
"jobs": Object {},
@@ -1070,12 +1123,57 @@ Object {
"scans": Array [],
"show": false,
},
- "pause": Object {},
- "restart": Object {},
- "start": Object {},
- "view": Object {
- "update": true,
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "NOT_EXPANDED_SCAN",
+}
+`;
+
+exports[`ScansReducer should handle specific defined types: defined type SELECT_SCAN 1`] = `
+Object {
+ "result": Object {
+ "action": Object {},
+ "connection": Object {},
+ "empty": Object {},
+ "expanded": Object {},
+ "inspection": Object {},
+ "job": Object {},
+ "jobs": Object {},
+ "mergeDialog": Object {
+ "details": false,
+ "scans": Array [],
+ "show": false,
+ },
+ "selected": Object {
+ "undefined": undefined,
+ },
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "SELECT_SCAN",
+}
+`;
+
+exports[`ScansReducer should handle specific defined types: defined type UPDATE_SCANS 1`] = `
+Object {
+ "result": Object {
+ "action": Object {},
+ "connection": Object {},
+ "empty": Object {},
+ "expanded": Object {},
+ "inspection": Object {},
+ "job": Object {},
+ "jobs": Object {},
+ "mergeDialog": Object {
+ "details": false,
+ "scans": Array [],
+ "show": false,
},
+ "selected": Object {},
+ "update": 1654041600000,
+ "view": Object {},
},
"type": "UPDATE_SCANS",
}
diff --git a/src/redux/reducers/__tests__/__snapshots__/sourcesReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/sourcesReducer.test.js.snap
index 79d177c9..0205eb73 100644
--- a/src/redux/reducers/__tests__/__snapshots__/sourcesReducer.test.js.snap
+++ b/src/redux/reducers/__tests__/__snapshots__/sourcesReducer.test.js.snap
@@ -1,68 +1,330 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`SourcesReducer should handle all defined error types: rejected types DELETE_SOURCE 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {
+ "error": true,
+ "errorMessage": "ERROR",
+ "errorStatus": 0,
+ "fulfilled": false,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
+ "pending": false,
+ "update": false,
+ },
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "DELETE_SOURCE_REJECTED",
+}
+`;
+
+exports[`SourcesReducer should handle all defined error types: rejected types DELETE_SOURCES 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {
+ "error": true,
+ "errorMessage": "ERROR",
+ "errorStatus": 0,
+ "fulfilled": false,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
+ "pending": false,
+ "update": false,
+ },
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "DELETE_SOURCES_REJECTED",
+}
+`;
+
exports[`SourcesReducer should handle all defined error types: rejected types GET_SOURCES 1`] = `
Object {
"result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {},
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {
"error": true,
"errorMessage": "ERROR",
+ "errorStatus": 0,
"fulfilled": false,
- "lastRefresh": 0,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
"pending": false,
- "sources": Array [],
- "updateSources": false,
+ "update": false,
},
},
"type": "GET_SOURCES_REJECTED",
}
`;
+exports[`SourcesReducer should handle all defined fulfilled types: fulfilled types DELETE_SOURCE 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {
+ "data": Object {
+ "test": "success",
+ },
+ "date": undefined,
+ "error": false,
+ "errorMessage": "",
+ "fulfilled": true,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
+ "pending": false,
+ "update": false,
+ },
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "DELETE_SOURCE_FULFILLED",
+}
+`;
+
+exports[`SourcesReducer should handle all defined fulfilled types: fulfilled types DELETE_SOURCES 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {
+ "data": Object {
+ "test": "success",
+ },
+ "date": undefined,
+ "error": false,
+ "errorMessage": "",
+ "fulfilled": true,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
+ "pending": false,
+ "update": false,
+ },
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "DELETE_SOURCES_FULFILLED",
+}
+`;
+
exports[`SourcesReducer should handle all defined fulfilled types: fulfilled types GET_SOURCES 1`] = `
Object {
"result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {},
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {
+ "data": Object {
+ "test": "success",
+ },
+ "date": undefined,
"error": false,
"errorMessage": "",
"fulfilled": true,
- "lastRefresh": 0,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
"pending": false,
- "sources": Array [],
- "updateSources": false,
+ "update": false,
},
},
"type": "GET_SOURCES_FULFILLED",
}
`;
-exports[`SourcesReducer should handle all defined pending types: pending types GET_SOURCES 1`] = `
+exports[`SourcesReducer should handle all defined pending types: pending types DELETE_SOURCE 1`] = `
Object {
"result": Object {
- "view": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {
"error": false,
"errorMessage": "",
"fulfilled": false,
- "lastRefresh": 0,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
"pending": true,
- "sources": Array [],
- "updateSources": false,
+ "update": false,
},
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
},
- "type": "GET_SOURCES_PENDING",
+ "type": "DELETE_SOURCE_PENDING",
}
`;
-exports[`SourcesReducer should handle specific defined types: defined type UPDATE_SOURCES 1`] = `
+exports[`SourcesReducer should handle all defined pending types: pending types DELETE_SOURCES 1`] = `
Object {
"result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {
+ "error": false,
+ "errorMessage": "",
+ "fulfilled": false,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
+ "pending": true,
+ "update": false,
+ },
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "DELETE_SOURCES_PENDING",
+}
+`;
+
+exports[`SourcesReducer should handle all defined pending types: pending types GET_SOURCES 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {},
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
"view": Object {
"error": false,
"errorMessage": "",
"fulfilled": false,
- "lastRefresh": 0,
- "pending": false,
- "sources": Array [],
- "updateSources": true,
+ "metaData": undefined,
+ "metaId": undefined,
+ "metaQuery": undefined,
+ "pending": true,
+ "update": false,
+ },
+ },
+ "type": "GET_SOURCES_PENDING",
+}
+`;
+
+exports[`SourcesReducer should handle specific defined types: defined type CONFIRM_DELETE_SOURCE 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {
+ "source": undefined,
+ },
+ "deleted": Object {},
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "CONFIRM_DELETE_SOURCE",
+}
+`;
+
+exports[`SourcesReducer should handle specific defined types: defined type DESELECT_SOURCE 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {},
+ "expanded": Object {},
+ "selected": Object {
+ "undefined": null,
+ },
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "DESELECT_SOURCE",
+}
+`;
+
+exports[`SourcesReducer should handle specific defined types: defined type EXPANDED_SOURCE 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {},
+ "expanded": Object {
+ "undefined": undefined,
},
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "EXPANDED_SOURCE",
+}
+`;
+
+exports[`SourcesReducer should handle specific defined types: defined type NOT_EXPANDED_SOURCE 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {},
+ "expanded": Object {
+ "undefined": null,
+ },
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "NOT_EXPANDED_SOURCE",
+}
+`;
+
+exports[`SourcesReducer should handle specific defined types: defined type RESET_DELETE_SOURCE 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {},
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "RESET_DELETE_SOURCE",
+}
+`;
+
+exports[`SourcesReducer should handle specific defined types: defined type SELECT_SOURCE 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {},
+ "expanded": Object {},
+ "selected": Object {
+ "undefined": undefined,
+ },
+ "update": 0,
+ "view": Object {},
+ },
+ "type": "SELECT_SOURCE",
+}
+`;
+
+exports[`SourcesReducer should handle specific defined types: defined type UPDATE_SOURCES 1`] = `
+Object {
+ "result": Object {
+ "confirmDelete": Object {},
+ "deleted": Object {},
+ "expanded": Object {},
+ "selected": Object {},
+ "update": 1654041600000,
+ "view": Object {},
},
"type": "UPDATE_SOURCES",
}
diff --git a/src/redux/reducers/__tests__/__snapshots__/toastNotificationsReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/toastNotificationsReducer.test.js.snap
index f39c7f2a..9ba3c8eb 100644
--- a/src/redux/reducers/__tests__/__snapshots__/toastNotificationsReducer.test.js.snap
+++ b/src/redux/reducers/__tests__/__snapshots__/toastNotificationsReducer.test.js.snap
@@ -3,7 +3,6 @@
exports[`toastNotificationsReducer should handle adding and removing toast notifications: toast added 1`] = `
Object {
"displayedToasts": 2,
- "paused": false,
"toasts": Array [
Object {
"alertType": "success",
@@ -24,7 +23,6 @@ Object {
exports[`toastNotificationsReducer should handle adding and removing toast notifications: toast removed 1`] = `
Object {
"displayedToasts": 2,
- "paused": false,
"toasts": Array [
Object {
"alertType": "success",
@@ -47,7 +45,6 @@ exports[`toastNotificationsReducer should handle specific defined types: defined
Object {
"result": Object {
"displayedToasts": 1,
- "paused": false,
"toasts": Array [
Object {
"alertType": undefined,
@@ -65,7 +62,6 @@ exports[`toastNotificationsReducer should handle specific defined types: defined
Object {
"result": Object {
"displayedToasts": 0,
- "paused": true,
"toasts": Array [],
},
"type": "TOAST_PAUSE",
@@ -76,7 +72,6 @@ exports[`toastNotificationsReducer should handle specific defined types: defined
Object {
"result": Object {
"displayedToasts": 0,
- "paused": false,
"toasts": Array [],
},
"type": "TOAST_REMOVE",
@@ -87,7 +82,6 @@ exports[`toastNotificationsReducer should handle specific defined types: defined
Object {
"result": Object {
"displayedToasts": 0,
- "paused": false,
"toasts": Array [],
},
"type": "TOAST_RESUME",
diff --git a/src/redux/reducers/__tests__/__snapshots__/viewOptionsReducer.test.js.snap b/src/redux/reducers/__tests__/__snapshots__/viewOptionsReducer.test.js.snap
index 0408ddd8..7041035f 100644
--- a/src/redux/reducers/__tests__/__snapshots__/viewOptionsReducer.test.js.snap
+++ b/src/redux/reducers/__tests__/__snapshots__/viewOptionsReducer.test.js.snap
@@ -9,7 +9,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -23,7 +23,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -37,7 +37,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -59,7 +59,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -73,7 +73,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -87,7 +87,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -109,7 +109,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -123,7 +123,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -137,7 +137,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -159,7 +159,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -173,7 +173,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -189,7 +189,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -211,7 +211,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -225,7 +225,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -239,7 +239,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -261,7 +261,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -275,7 +275,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -289,7 +289,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -311,7 +311,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -325,7 +325,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -339,7 +339,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -361,7 +361,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -375,7 +375,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -389,7 +389,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -411,7 +411,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -425,7 +425,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -439,7 +439,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [
undefined,
],
@@ -463,7 +463,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -477,7 +477,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -491,7 +491,7 @@ Object {
"expandedItems": Array [],
"filterType": undefined,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -513,7 +513,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -527,7 +527,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -541,7 +541,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": undefined,
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -563,7 +563,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -577,7 +577,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -613,7 +613,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -627,7 +627,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -641,7 +641,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": undefined,
@@ -663,7 +663,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -677,7 +677,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -691,7 +691,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": false,
"sortField": "name",
@@ -704,7 +704,7 @@ Object {
}
`;
-exports[`viewOptionsReducer should handle specific defined types: defined type VIEW_FIRST_PAGE 1`] = `
+exports[`viewOptionsReducer should handle specific defined types: defined type VIEW_PAGE 1`] = `
Object {
"result": Object {
"CREDENTIALS_VIEW": Object {
@@ -713,7 +713,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -727,7 +727,7 @@ Object {
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -737,211 +737,11 @@ Object {
},
"SOURCES_VIEW": Object {
"activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- },
- "type": "VIEW_FIRST_PAGE",
-}
-`;
-
-exports[`viewOptionsReducer should handle specific defined types: defined type VIEW_LAST_PAGE 1`] = `
-Object {
- "result": Object {
- "CREDENTIALS_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- "SCANS_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- "SOURCES_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 0,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- },
- "type": "VIEW_LAST_PAGE",
-}
-`;
-
-exports[`viewOptionsReducer should handle specific defined types: defined type VIEW_NEXT_PAGE 1`] = `
-Object {
- "result": Object {
- "CREDENTIALS_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- "SCANS_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- "SOURCES_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- },
- "type": "VIEW_NEXT_PAGE",
-}
-`;
-
-exports[`viewOptionsReducer should handle specific defined types: defined type VIEW_PAGE_NUMBER 1`] = `
-Object {
- "result": Object {
- "CREDENTIALS_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- "SCANS_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- "SOURCES_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- },
- "type": "VIEW_PAGE_NUMBER",
-}
-`;
-
-exports[`viewOptionsReducer should handle specific defined types: defined type VIEW_PREVIOUS_PAGE 1`] = `
-Object {
- "result": Object {
- "CREDENTIALS_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- "SCANS_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
- "expandedItems": Array [],
- "filterType": null,
- "filterValue": "",
- "pageSize": 15,
- "selectedItems": Array [],
- "sortAscending": true,
- "sortField": "name",
- "sortType": null,
- "totalCount": 0,
- "totalPages": 0,
- },
- "SOURCES_VIEW": Object {
- "activeFilters": Array [],
- "currentPage": 1,
+ "currentPage": undefined,
"expandedItems": Array [],
"filterType": null,
"filterValue": "",
- "pageSize": 15,
+ "pageSize": 10,
"selectedItems": Array [],
"sortAscending": true,
"sortField": "name",
@@ -950,6 +750,6 @@ Object {
"totalPages": 0,
},
},
- "type": "VIEW_PREVIOUS_PAGE",
+ "type": "VIEW_PAGE",
}
`;
diff --git a/src/redux/reducers/__tests__/scansReducer.test.js b/src/redux/reducers/__tests__/scansReducer.test.js
index 23ea4fe3..ca6b71b2 100644
--- a/src/redux/reducers/__tests__/scansReducer.test.js
+++ b/src/redux/reducers/__tests__/scansReducer.test.js
@@ -8,7 +8,15 @@ describe('ScansReducer', () => {
});
it('should handle specific defined types', () => {
- const specificTypes = [types.UPDATE_SCANS, types.MERGE_SCAN_DIALOG_SHOW, types.MERGE_SCAN_DIALOG_HIDE];
+ const specificTypes = [
+ types.UPDATE_SCANS,
+ types.MERGE_SCAN_DIALOG_SHOW,
+ types.MERGE_SCAN_DIALOG_HIDE,
+ types.SELECT_SCAN,
+ types.DESELECT_SCAN,
+ types.EXPANDED_SCAN,
+ types.NOT_EXPANDED_SCAN
+ ];
specificTypes.forEach(value => {
const dispatched = {
diff --git a/src/redux/reducers/__tests__/sourcesReducer.test.js b/src/redux/reducers/__tests__/sourcesReducer.test.js
index fead60e3..58230495 100644
--- a/src/redux/reducers/__tests__/sourcesReducer.test.js
+++ b/src/redux/reducers/__tests__/sourcesReducer.test.js
@@ -8,7 +8,15 @@ describe('SourcesReducer', () => {
});
it('should handle specific defined types', () => {
- const specificTypes = [types.UPDATE_SOURCES];
+ const specificTypes = [
+ types.UPDATE_SOURCES,
+ types.CONFIRM_DELETE_SOURCE,
+ types.RESET_DELETE_SOURCE,
+ types.SELECT_SOURCE,
+ types.DESELECT_SOURCE,
+ types.EXPANDED_SOURCE,
+ types.NOT_EXPANDED_SOURCE
+ ];
specificTypes.forEach(value => {
const dispatched = {
@@ -22,7 +30,7 @@ describe('SourcesReducer', () => {
});
it('should handle all defined error types', () => {
- const specificTypes = [types.GET_SOURCES];
+ const specificTypes = [types.GET_SOURCES, types.DELETE_SOURCE, types.DELETE_SOURCES];
specificTypes.forEach(value => {
const dispatched = {
@@ -49,7 +57,7 @@ describe('SourcesReducer', () => {
});
it('should handle all defined pending types', () => {
- const specificTypes = [types.GET_SOURCES];
+ const specificTypes = [types.GET_SOURCES, types.DELETE_SOURCE, types.DELETE_SOURCES];
specificTypes.forEach(value => {
const dispatched = {
@@ -65,7 +73,7 @@ describe('SourcesReducer', () => {
});
it('should handle all defined fulfilled types', () => {
- const specificTypes = [types.GET_SOURCES];
+ const specificTypes = [types.GET_SOURCES, types.DELETE_SOURCE, types.DELETE_SOURCES];
specificTypes.forEach(value => {
const dispatched = {
diff --git a/src/redux/reducers/__tests__/viewOptionsReducer.test.js b/src/redux/reducers/__tests__/viewOptionsReducer.test.js
index 19135606..5b5de758 100644
--- a/src/redux/reducers/__tests__/viewOptionsReducer.test.js
+++ b/src/redux/reducers/__tests__/viewOptionsReducer.test.js
@@ -23,11 +23,7 @@ describe('viewOptionsReducer', () => {
viewToolbarTypes.CLEAR_FILTERS,
viewToolbarTypes.SET_SORT_TYPE,
viewToolbarTypes.TOGGLE_SORT_ASCENDING,
- viewPaginationTypes.VIEW_FIRST_PAGE,
- viewPaginationTypes.VIEW_LAST_PAGE,
- viewPaginationTypes.VIEW_PREVIOUS_PAGE,
- viewPaginationTypes.VIEW_NEXT_PAGE,
- viewPaginationTypes.VIEW_PAGE_NUMBER,
+ viewPaginationTypes.VIEW_PAGE,
viewPaginationTypes.SET_PER_PAGE,
viewTypes.SELECT_ITEM,
viewTypes.DESELECT_ITEM,
diff --git a/src/redux/reducers/addSourceWizardReducer.js b/src/redux/reducers/addSourceWizardReducer.js
index 6fd48c74..4a66af9b 100644
--- a/src/redux/reducers/addSourceWizardReducer.js
+++ b/src/redux/reducers/addSourceWizardReducer.js
@@ -168,13 +168,15 @@ const addSourceWizardReducer = (state = initialState, action) => {
{
add: state.add,
edit: state.edit,
+ error: false,
+ errorMessage: null,
+ pending: false,
fulfilled: true,
- show: true,
source: action.payload.data
},
{
state,
- initialState
+ reset: false
}
);
diff --git a/src/redux/reducers/confirmationModalReducer.js b/src/redux/reducers/confirmationModalReducer.js
index 6cb710ff..bc2f2c89 100644
--- a/src/redux/reducers/confirmationModalReducer.js
+++ b/src/redux/reducers/confirmationModalReducer.js
@@ -23,7 +23,8 @@ const confirmationModalReducer = (state = initialState, action) => {
confirmButtonText: action.confirmButtonText || 'Confirm',
cancelButtonText: action.cancelButtonText || 'Cancel',
onConfirm: action.onConfirm,
- onCancel: action.onCancel
+ onCancel: action.onCancel,
+ variant: action.variant
};
case confirmationModalTypes.CONFIRMATION_MODAL_HIDE:
diff --git a/src/redux/reducers/scansReducer.js b/src/redux/reducers/scansReducer.js
index df667158..69b6a6d6 100644
--- a/src/redux/reducers/scansReducer.js
+++ b/src/redux/reducers/scansReducer.js
@@ -1,5 +1,6 @@
import { scansTypes, sourcesTypes } from '../constants';
-import { reduxHelpers } from '../common/reduxHelpers';
+import { reduxHelpers } from '../common';
+import { helpers } from '../../common';
const initialState = {
mergeDialog: {
@@ -7,16 +8,15 @@ const initialState = {
scans: [],
details: false
},
-
empty: {},
connection: {},
inspection: {},
job: {},
jobs: {},
- cancel: {},
- pause: {},
- restart: {},
- start: {},
+ action: {},
+ selected: {},
+ expanded: {},
+ update: 0,
view: {}
};
@@ -24,9 +24,9 @@ const scansReducer = (state = initialState, action) => {
switch (action.type) {
case scansTypes.UPDATE_SCANS:
return reduxHelpers.setStateProp(
- 'view',
+ null,
{
- update: true
+ update: helpers.getCurrentDate().getTime()
},
{
state,
@@ -60,6 +60,51 @@ const scansReducer = (state = initialState, action) => {
}
);
+ case scansTypes.SELECT_SCAN:
+ return reduxHelpers.setStateProp(
+ 'selected',
+ {
+ [action.item?.id]: action.item
+ },
+ {
+ state,
+ reset: false
+ }
+ );
+ case scansTypes.DESELECT_SCAN:
+ return reduxHelpers.setStateProp(
+ 'selected',
+ {
+ [action.item?.id]: null
+ },
+ {
+ state,
+ reset: false
+ }
+ );
+ case scansTypes.EXPANDED_SCAN:
+ return reduxHelpers.setStateProp(
+ 'expanded',
+ {
+ [action.item?.id]: action.cellIndex
+ },
+ {
+ state,
+ reset: false
+ }
+ );
+ case scansTypes.NOT_EXPANDED_SCAN:
+ return reduxHelpers.setStateProp(
+ 'expanded',
+ {
+ [action.item?.id]: null
+ },
+ {
+ state,
+ reset: false
+ }
+ );
+
default:
return reduxHelpers.generatedPromiseActionReducer(
[
@@ -68,10 +113,10 @@ const scansReducer = (state = initialState, action) => {
{ ref: 'inspection', type: scansTypes.GET_SCAN_INSPECTION_RESULTS },
{ ref: 'job', type: scansTypes.GET_SCAN_JOB },
{ ref: 'jobs', type: scansTypes.GET_SCAN_JOBS },
- { ref: 'cancel', type: scansTypes.CANCEL_SCAN },
- { ref: 'pause', type: scansTypes.PAUSE_SCAN },
- { ref: 'restart', type: scansTypes.RESTART_SCAN },
- { ref: 'start', type: scansTypes.START_SCAN },
+ {
+ ref: 'action',
+ type: [scansTypes.CANCEL_SCAN, scansTypes.PAUSE_SCAN, scansTypes.RESTART_SCAN, scansTypes.START_SCAN]
+ },
{ ref: 'view', type: scansTypes.GET_SCANS }
],
state,
diff --git a/src/redux/reducers/sourcesReducer.js b/src/redux/reducers/sourcesReducer.js
index 73c47f96..567d220b 100644
--- a/src/redux/reducers/sourcesReducer.js
+++ b/src/redux/reducers/sourcesReducer.js
@@ -1,76 +1,105 @@
import { sourcesTypes } from '../constants';
-import { helpers } from '../../common/helpers';
-import { reduxHelpers } from '../common/reduxHelpers';
-import apiTypes from '../../constants/apiConstants';
+import { helpers } from '../../common';
+import { reduxHelpers } from '../common';
const initialState = {
- view: {
- error: false,
- errorMessage: '',
- pending: false,
- fulfilled: false,
- lastRefresh: 0,
- sources: [],
- updateSources: false
- }
+ confirmDelete: {},
+ deleted: {},
+ selected: {},
+ expanded: {},
+ update: 0,
+ view: {}
};
const sourcesReducer = (state = initialState, action) => {
switch (action.type) {
case sourcesTypes.UPDATE_SOURCES:
return reduxHelpers.setStateProp(
- 'view',
+ null,
{
- updateSources: true
+ update: helpers.getCurrentDate().getTime()
},
{
state,
reset: false
}
);
-
- case reduxHelpers.REJECTED_ACTION(sourcesTypes.GET_SOURCES):
+ case sourcesTypes.CONFIRM_DELETE_SOURCE:
return reduxHelpers.setStateProp(
- 'view',
+ 'confirmDelete',
{
- error: action.error,
- errorMessage: helpers.getMessageFromResults(action.payload).message
+ source: action.source
},
{
state,
initialState
}
);
-
- case reduxHelpers.PENDING_ACTION(sourcesTypes.GET_SOURCES):
+ case sourcesTypes.RESET_DELETE_SOURCE:
return reduxHelpers.setStateProp(
- 'view',
+ null,
{
- pending: true,
- sources: state.view.sources
+ confirmDelete: {},
+ deleted: {}
},
{
state,
initialState
}
);
-
- case reduxHelpers.FULFILLED_ACTION(sourcesTypes.GET_SOURCES):
+ case sourcesTypes.SELECT_SOURCE:
return reduxHelpers.setStateProp(
- 'view',
+ 'selected',
{
- fulfilled: true,
- lastRefresh: (action.payload.headers && new Date(action.payload.headers.date).getTime()) || 0,
- sources: (action.payload.data && action.payload.data[apiTypes.API_RESPONSE_SOURCES_RESULTS]) || []
+ [action.source?.id]: action.source
},
{
state,
- initialState
+ reset: false
+ }
+ );
+ case sourcesTypes.DESELECT_SOURCE:
+ return reduxHelpers.setStateProp(
+ 'selected',
+ {
+ [action.source?.id]: null
+ },
+ {
+ state,
+ reset: false
+ }
+ );
+ case sourcesTypes.EXPANDED_SOURCE:
+ return reduxHelpers.setStateProp(
+ 'expanded',
+ {
+ [action.source?.id]: action.cellIndex
+ },
+ {
+ state,
+ reset: false
+ }
+ );
+ case sourcesTypes.NOT_EXPANDED_SOURCE:
+ return reduxHelpers.setStateProp(
+ 'expanded',
+ {
+ [action.source?.id]: null
+ },
+ {
+ state,
+ reset: false
}
);
-
default:
- return state;
+ return reduxHelpers.generatedPromiseActionReducer(
+ [
+ { ref: 'deleted', type: [sourcesTypes.DELETE_SOURCE, sourcesTypes.DELETE_SOURCES] },
+ { ref: 'view', type: sourcesTypes.GET_SOURCES }
+ ],
+ state,
+ action
+ );
}
};
diff --git a/src/redux/reducers/toastNotificationsReducer.js b/src/redux/reducers/toastNotificationsReducer.js
index d5198d4c..24c821d7 100644
--- a/src/redux/reducers/toastNotificationsReducer.js
+++ b/src/redux/reducers/toastNotificationsReducer.js
@@ -2,7 +2,6 @@ import { toastNotificationTypes } from '../constants';
const initialState = {
toasts: [],
- paused: false,
displayedToasts: 0
};
@@ -41,18 +40,32 @@ const toastNotificationsReducer = (state = initialState, action) => {
};
case toastNotificationTypes.TOAST_PAUSE:
+ const pausedToasts = [...state.toasts];
+ const pausedToastIndex = state.toasts.indexOf(action.toast);
+
+ if (pausedToastIndex > -1) {
+ pausedToasts[pausedToastIndex].paused = true;
+ }
+
return {
...state,
...{
- paused: true
+ toasts: pausedToasts
}
};
case toastNotificationTypes.TOAST_RESUME:
+ const resumedToasts = [...state.toasts];
+ const resumedToastIndex = state.toasts.indexOf(action.toast);
+
+ if (resumedToastIndex > -1) {
+ resumedToasts[resumedToastIndex].paused = false;
+ }
+
return {
...state,
...{
- paused: false
+ toasts: resumedToasts
}
};
diff --git a/src/redux/reducers/viewOptionsReducer.js b/src/redux/reducers/viewOptionsReducer.js
index 70f0f426..a50cfeaa 100644
--- a/src/redux/reducers/viewOptionsReducer.js
+++ b/src/redux/reducers/viewOptionsReducer.js
@@ -15,7 +15,7 @@ const initialState = {};
const INITAL_VIEW_STATE = {
currentPage: 1,
- pageSize: 15,
+ pageSize: 10,
totalCount: 0,
totalPages: 0,
filterType: null,
@@ -143,39 +143,8 @@ const viewOptionsReducer = (state = initialState, action) => {
};
return { ...state, ...updateState };
- case viewPaginationTypes.VIEW_FIRST_PAGE:
- updateState[action.viewType] = { ...state[action.viewType], currentPage: 1 };
- return { ...state, ...updateState };
-
- case viewPaginationTypes.VIEW_LAST_PAGE:
- updateState[action.viewType] = { ...state[action.viewType], currentPage: state[action.viewType].totalPages };
- return { ...state, ...updateState };
-
- case viewPaginationTypes.VIEW_PREVIOUS_PAGE:
- if (state[action.viewType].currentPage < 2) {
- return state;
- }
-
- updateState[action.viewType] = { ...state[action.viewType], currentPage: state[action.viewType].currentPage - 1 };
- return { ...state, ...updateState };
-
- case viewPaginationTypes.VIEW_NEXT_PAGE:
- if (state[action.viewType].currentPage >= state[action.viewType].totalPages) {
- return state;
- }
- updateState[action.viewType] = { ...state[action.viewType], currentPage: state[action.viewType].currentPage + 1 };
- return { ...state, ...updateState };
-
- case viewPaginationTypes.VIEW_PAGE_NUMBER:
- if (
- !Number.isInteger(action.pageNumber) ||
- action.pageNumber < 1 ||
- action.pageNumber > state[action.viewType].totalPages
- ) {
- return state;
- }
-
- updateState[action.viewType] = { ...state[action.viewType], currentPage: action.pageNumber };
+ case viewPaginationTypes.VIEW_PAGE:
+ updateState[action.viewType] = { ...state[action.viewType], currentPage: action.currentPage };
return { ...state, ...updateState };
case viewPaginationTypes.SET_PER_PAGE:
diff --git a/src/redux/selectors/credentialsSelectors.js b/src/redux/selectors/credentialsSelectors.js
index 3f788897..4b25d785 100644
--- a/src/redux/selectors/credentialsSelectors.js
+++ b/src/redux/selectors/credentialsSelectors.js
@@ -3,6 +3,9 @@ import apiTypes from '../../constants/apiConstants';
/**
* Map a credential array to a consumable dropdown format
+ *
+ * @param {object} state
+ * @returns {*}
*/
const credentials = state => state.credentials.view.credentials;
diff --git a/src/redux/selectors/scansSelectors.js b/src/redux/selectors/scansSelectors.js
index b442912b..4036df06 100644
--- a/src/redux/selectors/scansSelectors.js
+++ b/src/redux/selectors/scansSelectors.js
@@ -6,6 +6,8 @@ import { apiTypes } from '../../constants/apiConstants';
/**
* Map a hosts object to consumable prop names
+ *
+ * @type {{}}
*/
const scanHostsListSelectorCache = {};
@@ -131,6 +133,10 @@ const makeScanHostsListSelector = () => scanHostsListSelector;
/**
* Map a job object to consumable prop names and sorted by source
+ *
+ * @param {object} state
+ * @param {object} props
+ * @returns {*}
*/
const scanJobDetail = (state, props) => state.scans.job[props.id];
@@ -220,6 +226,8 @@ const makeScanJobDetailBySourceSelector = () => scanJobDetailBySourceSelector;
/**
* Map a jobs object to consumable prop names
+ *
+ * @type {{}}
*/
const previousScansSelectorsCache = {};
@@ -283,6 +291,10 @@ const makeScanJobsListSelector = () => scanJobsListSelector;
/**
* Map a scan object to consumable prop names
+ *
+ * @param {object} state
+ * @param {object} props
+ * @returns {*}
*/
const scanListItem = (state, props) => props;
@@ -358,14 +370,19 @@ const makeScanListItemSelector = () => scanListItemSelector;
/**
* Return a check for sources existing
+ *
+ * @param {object} state
+ * @returns {*}
*/
const scansEmptyState = state => state.scans.empty;
const scansEmptyStateSelector = createSelector([scansEmptyState], empty => {
- const sourcesExist = (empty.data && empty.data[apiTypes.API_RESPONSE_SOURCES_COUNT]) > 0;
+ const sourcesCount = empty?.data?.[apiTypes.API_RESPONSE_SOURCES_COUNT];
+ const sourcesExist = sourcesCount > 0;
return {
...empty,
+ sourcesCount,
sourcesExist
};
});
@@ -374,6 +391,9 @@ const makeScansEmptyStateSelector = () => scansEmptyStateSelector;
/**
* Return a list of scan objects
+ *
+ * @param {object} state
+ * @returns {*}
*/
const scansView = state => state.scans.view;
diff --git a/src/redux/selectors/sourcesSelectors.js b/src/redux/selectors/sourcesSelectors.js
index 2e3517be..714283d0 100644
--- a/src/redux/selectors/sourcesSelectors.js
+++ b/src/redux/selectors/sourcesSelectors.js
@@ -5,11 +5,17 @@ import apiTypes from '../../constants/apiConstants';
/**
* Map a new source object to consumable prop names
+ *
+ * @param {object} state
+ * @returns {*}
*/
const sourceDetail = state => state.addSourceWizard.source;
/**
* Map an edit source object to consumable prop names
+ *
+ * @param {object} state
+ * @returns {*}
*/
const editSourceDetail = state => state.addSourceWizard.editSource;
diff --git a/src/services/config.js b/src/services/config.js
index c8e22855..2d4510aa 100644
--- a/src/services/config.js
+++ b/src/services/config.js
@@ -9,7 +9,7 @@ const globalXhrTimeout = Number.parseInt(process.env.REACT_APP_AJAX_TIMEOUT, 10)
/**
* Return a formatted auth header.
*
- * @return {{}}
+ * @returns {{}}
*/
const authHeader = () => {
const authToken = cookies.get(process.env.REACT_APP_AUTH_TOKEN) || '';
diff --git a/src/services/userService.js b/src/services/userService.js
index 72d37c71..79b292fd 100644
--- a/src/services/userService.js
+++ b/src/services/userService.js
@@ -6,6 +6,7 @@ import { serviceCall } from './config';
*/
/**
* Get the users locale
+ *
* @returns {Promise}
*/
const getLocale = () => {
diff --git a/src/setupTests.js b/src/setupTests.js
index eabaf26b..f6738cf4 100644
--- a/src/setupTests.js
+++ b/src/setupTests.js
@@ -18,6 +18,14 @@ jest.mock('i18next', () => {
return new Test();
});
+/**
+ * We currently use a wrapper for useSelector, emulate for component checks
+ */
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest.fn()
+}));
+
/**
* Enzyme for components using hooks.
*
diff --git a/src/styles/app/_aboutModal.scss b/src/styles/app/_aboutModal.scss
index 198d1f13..419d876c 100644
--- a/src/styles/app/_aboutModal.scss
+++ b/src/styles/app/_aboutModal.scss
@@ -1,26 +1,31 @@
-.quipucords-about-modal-copy-footer {
- margin-top: -28px;
- text-align: right;
-}
-
-.quipucords-about-modal-copy-button {
- box-shadow: 0 0 3px $color-pf-black-300;
-}
+.quipucords-about-modal {
+ .pf-c-content {
+ font-size: inherit;
+ }
-.quipucords-about-modal-list {
- box-shadow: 0 0 2px $color-pf-black-300;
- padding: 10px;
+ .quipucords-about-modal-copy-footer {
+ text-align: right;
+ }
- &:focus,
- &:hover {
- box-shadow: 0 0 5px $color-pf-black-100;
+ .quipucords-about-modal-copy-button {
+ box-shadow: 0 0 3px var(--pf-global--palette--black-300);
}
- .list-unstyled li {
- display: flex;
+ .quipucords-about-modal-list {
+ box-shadow: 0 0 2px var(--pf-global--palette--black-300);
+ padding: 10px;
+
+ &:focus,
+ &:hover {
+ box-shadow: 0 0 5px var(--pf-global--palette--black-100);
+ }
+
+ .list-unstyled li {
+ display: flex;
- strong {
- white-space: nowrap;
+ strong {
+ white-space: nowrap;
+ }
}
}
}
diff --git a/src/styles/app/_app.scss b/src/styles/app/_app.scss
index 79428833..dcdf257a 100644
--- a/src/styles/app/_app.scss
+++ b/src/styles/app/_app.scss
@@ -1,4 +1,6 @@
/* _app.scss */
+
+//ToDo: investigate where these `.App` and `.App-` prefixed classes are used?
.App {
text-align: center;
}
diff --git a/src/styles/app/_dropdownSelect.scss b/src/styles/app/_dropdownSelect.scss
new file mode 100644
index 00000000..3f9f0f64
--- /dev/null
+++ b/src/styles/app/_dropdownSelect.scss
@@ -0,0 +1,23 @@
+.quipucords-select {
+ position: relative;
+
+ &-pf__no-toggle-text {
+ span.pf-c-select__toggle-arrow {
+ margin-left: var(--pf-c-select__toggle-arrow--MarginRight);
+ }
+
+ span.pf-c-select__toggle-text {
+ display: none;
+ }
+ }
+
+ &-pf__position-right {
+ > .pf-c-select__menu {
+ right: 0;
+ }
+ }
+
+ &__inline {
+ display: inline-block;
+ }
+}
diff --git a/src/styles/app/_emptyState.scss b/src/styles/app/_emptyState.scss
new file mode 100644
index 00000000..8fc2c091
--- /dev/null
+++ b/src/styles/app/_emptyState.scss
@@ -0,0 +1,18 @@
+.quipucords-empty-state {
+ .pf-c-empty-state__primary .pf-c-button,
+ .pf-c-empty-state__secondary .pf-c-button,
+ .pf-c-empty-state__body {
+ font-size: inherit;
+ }
+
+ &__alert {
+ .pf-c-empty-state__content {
+ text-align: left;
+ --pf-c-empty-state__content--MaxWidth: var(--pf-c-empty-state--m-lg__content--MaxWidth);
+
+ @media (min-width: $pf-global--breakpoint--md) {
+ min-width: var(--pf-c-empty-state__content--MaxWidth);
+ }
+ }
+ }
+}
diff --git a/src/styles/app/_forms.scss b/src/styles/app/_forms.scss
index d012772e..68c72236 100644
--- a/src/styles/app/_forms.scss
+++ b/src/styles/app/_forms.scss
@@ -1,7 +1,7 @@
/* _forms.scss */
.quipucords {
.has-error textarea.form-control {
- border-color: $color-pf-red-100;
+ border-color: var(--pf-global--palette--red-100);
}
}
@@ -9,48 +9,6 @@ textarea.vertical-scroll {
resize: vertical;
}
-.quipucords-dropdownselect {
- &.dropdown {
- width:100%;
-
- .dropdown-menu,
- .dropdown-toggle.btn {
- text-align: inherit;
- width: 100%;
- }
-
- .dropdown-toggle.btn {
- display: flex;
- flex-wrap: nowrap;
- align-items: center;
-
- > span:first-child {
- flex-direction: column;
- flex: 2;
- overflow: hidden;
- text-overflow: ellipsis;
- width: 1px;
- }
- }
-
- .quipucords-dropdownselect-menuitem {
- margin-left: 0;
- margin-right: 0;
- }
-
- .quipucords-dropdownselect-menuitemname {
- padding-left: 0;
- padding-right: 0;
- }
-
- .quipucords-dropdownselect-menuitemcheck {
- padding-left: 0;
- padding-right: 0;
- text-align: right;
- }
- }
-}
-
.cloudmeter-touchspin {
&.cloudmeter-scan-dialog-touchspin {
width: 30%;
@@ -68,7 +26,7 @@ textarea.vertical-scroll {
}
.quipucords-form-control.form-control[readonly] {
- background-color: $color-pf-white;
+ background-color: var(--pf-global--palette--white);
border: 0;
color: inherit;
font-weight: 700;
diff --git a/src/styles/app/_login.scss b/src/styles/app/_login.scss
index 676d6f1b..aa04b7d3 100644
--- a/src/styles/app/_login.scss
+++ b/src/styles/app/_login.scss
@@ -1,7 +1,7 @@
/* _login.scss */
.login .alert {
background: transparent;
- color: $color-pf-white;
+ color: var(--pf-global--palette--white);
}
#brand.login-title img {}
diff --git a/src/styles/app/_modal.scss b/src/styles/app/_modal.scss
new file mode 100644
index 00000000..17497bce
--- /dev/null
+++ b/src/styles/app/_modal.scss
@@ -0,0 +1,40 @@
+.quipucords{
+ &-modal {
+ h4.pf-c-title.pf-m-md {
+ font-size: 0.9rem;
+ }
+
+ .pf-c-button {
+ }
+
+ &__rcue-width {
+ div.pf-c-modal-box.pf-m-align-top {
+ @media (min-width: $pf-global--breakpoint--md) {
+ width: auto;
+ min-width: 600px;
+ }
+ }
+ }
+
+ &__hide-backdrop {
+ .pf-c-backdrop {
+ background-color: transparent;
+ }
+ }
+
+ &__confirmation {
+ .pf-c-backdrop {
+ z-index: calc(var(--pf-c-backdrop--ZIndex) + 1);
+ }
+ .pf-c-modal-box__body {
+ padding-bottom: var(--pf-c-modal-box__body--PaddingTop);
+ }
+ }
+
+ &__wizard {
+ .pf-c-backdrop {
+ z-index: calc(var(--pf-c-backdrop--ZIndex) - 1);
+ }
+ }
+ }
+}
diff --git a/src/styles/app/_overrides.scss b/src/styles/app/_overrides.scss
index de871af8..98662f8a 100644
--- a/src/styles/app/_overrides.scss
+++ b/src/styles/app/_overrides.scss
@@ -1,13 +1,4 @@
/* _overrides.scss */
-.about-modal-pf {
- background-color: #1a1a1a;
- background-image: url('../images/about-bg.png');
- background-size: 50% auto;
-
- .modal-footer > img {
- height: 28px;
- }
-}
.navbar-pf-vertical .navbar-brand .navbar-brand-name {
height: 26px;
diff --git a/src/styles/app/_patternflyOverrides.scss b/src/styles/app/_patternflyOverrides.scss
new file mode 100644
index 00000000..5d7ef1e4
--- /dev/null
+++ b/src/styles/app/_patternflyOverrides.scss
@@ -0,0 +1,10 @@
+// Global fontsize
+.cards-pf {
+ --pf-global--FontSize--md: 0.8rem;
+}
+
+// rcue overrides, remove as part of clean up
+.pf-c-select.pf-m-expanded input[type="radio"],
+.pf-c-select.pf-m-expanded input[type="checkbox"] {
+ margin-top: inherit;
+}
diff --git a/src/styles/app/_scans.scss b/src/styles/app/_scans.scss
index 841c54fc..8b622853 100644
--- a/src/styles/app/_scans.scss
+++ b/src/styles/app/_scans.scss
@@ -22,7 +22,7 @@
.scan-job-status-icon {
margin-right: 5px;
&.systems:before, &.systems:before {
- color: $color-pf-black;
+ color: var(--pf-global--palette--black-1000);
}
}
diff --git a/src/styles/app/_table.scss b/src/styles/app/_table.scss
new file mode 100644
index 00000000..2f69f3a8
--- /dev/null
+++ b/src/styles/app/_table.scss
@@ -0,0 +1,24 @@
+.quipucords-table {
+ // medium breakpoint for pf takes over
+ &__td.pf-m-width {
+ @for $tdWidth from 1 through 9 {
+ &-#{$tdWidth} {
+ width: percentage($tdWidth/100);
+ }
+ }
+ }
+
+ &__td-select {
+ &:first-child:after {
+ @media (max-width: $pf-global--breakpoint--md) {
+ border-left-width: 0 !important;
+ }
+ }
+ }
+
+ &__td-expand-content {
+ overflow-y: auto;
+ min-height: 4em;
+ max-height: 18em;
+ }
+}
diff --git a/src/styles/app/_toastNotifications.scss b/src/styles/app/_toastNotifications.scss
new file mode 100644
index 00000000..1041124b
--- /dev/null
+++ b/src/styles/app/_toastNotifications.scss
@@ -0,0 +1,10 @@
+/* _toastNotifications.scss */
+
+.quipucords-toast-notifications {
+ &__alert-group {
+ width: 25rem !important;
+ max-width: 25rem !important;
+ }
+
+ // space for more css class variations
+}
diff --git a/src/styles/app/_tooltip.scss b/src/styles/app/_tooltip.scss
new file mode 100644
index 00000000..313cd8c1
--- /dev/null
+++ b/src/styles/app/_tooltip.scss
@@ -0,0 +1,6 @@
+.quipucords {
+ &-tooltip__wrapper,
+ &-popover__wrapper {
+ display: inline-block;
+ }
+}
diff --git a/src/styles/app/_vars.scss b/src/styles/app/_vars.scss
new file mode 100644
index 00000000..2bd37af2
--- /dev/null
+++ b/src/styles/app/_vars.scss
@@ -0,0 +1,5 @@
+:root {
+ --quipucords--gray-dark: #1a1a1a;
+ // list-view-divider - #d1d1d1;
+ --quipucords--gray-light: #{$list-view-divider};
+}
diff --git a/src/styles/app/_views.scss b/src/styles/app/_views.scss
index 18c1adbc..58522efd 100644
--- a/src/styles/app/_views.scss
+++ b/src/styles/app/_views.scss
@@ -17,7 +17,7 @@
}
.quipucords-view-container {
- background-color: $color-pf-white;
+ background-color: var(--pf-global--palette--white);
display: flex;
flex-direction: column;
height: 100%;
@@ -33,7 +33,7 @@
}
.last-refresh-time {
- font-size: $font-size-small;
+ font-size: var(--pf-global--FontSize--xs);
margin-left: 5px;
position: relative;
top: -1px;
@@ -54,18 +54,19 @@
}
.quipicords-list-view {
+ margin-top: 15px;
.list-view-pf-actions {
margin-left: 0;
text-align: right;
- width: 150px;
+ min-width: 150px;
.btn-link {
- color: $color-pf-black-700;
+ color: var(--pf-global--palette--black-700);
.pficon, .fa {
font-size: 14px;
}
&:hover, &:focus {
- color: $color-pf-blue;
+ color: var(--pf-global--palette--blue-500);
}
}
.dropdown.btn-group {
@@ -96,9 +97,9 @@
flex: 1 1 auto;
.dropdown.btn-group {
.dropdown-toggle {
- color: $color-pf-black-700;
+ color: var(--pf-global--palette--black-700);
&:hover, &:focus {
- color: $color-pf-blue;
+ color: var(--pf-global--palette--blue-500);
}
}
}
@@ -116,6 +117,7 @@
}
}
+ // FixMe: consolidate both quipucords-infinite-results and quipucords-infinite-list into a single css class
.quipucords-infinite-results {
width: 100%;
}
@@ -124,19 +126,19 @@
overflow-y: auto;
min-height: 70px;
max-height: 300px;
- border: solid 1px $list-view-divider;
+ border: solid 1px var(--quipucords--gray-light);
border-left: 0;
border-right: 0;
margin-top: 25px;
padding: 5px 20px 5px 10px;
.btn-link {
- color: $color-pf-black-700;
+ color: var(--pf-global--palette--black-700);
.pficon, .fa {
font-size: 14px;
}
&:hover, &:focus {
- color: $color-pf-blue;
+ color: var(--pf-global--palette--blue-500);
}
}
}
@@ -171,11 +173,6 @@
}
}
-
-.list-view-pagination-top {
- padding: 0 14px;
-}
-
.toolbar-pf-results .quipucords-view-count {
float: right;
margin-right: 0;
@@ -190,7 +187,7 @@
width: 50%;
margin-right: 10px;
.list-group-item-heading {
- margin-right: 0px;
+ margin-right: 0;
.btn-link {
font-size: inherit;
@@ -200,7 +197,7 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- margin-right: 0px;
+ margin-right: 0;
}
}
@@ -225,10 +222,6 @@
}
}
-.quipucords-sources-network-button {
- padding-left: 0;
-}
-
.blank-slate-pf.full-page-blank-slate {
border: none;
}
@@ -278,5 +271,13 @@
}
.pficon.pficon-disconnected.is-error::before {
- color: $color-pf-red-100;
+ color: var(--pf-global--palette--red-100);
+}
+
+.quipucords-view__pagination {
+ padding-bottom: 0 !important;
+}
+
+.quipucords-view__row-button {
+ margin-left: inherit !important;
}
diff --git a/src/styles/app/_wizard.scss b/src/styles/app/_wizard.scss
index 433417da..a759c6c8 100644
--- a/src/styles/app/_wizard.scss
+++ b/src/styles/app/_wizard.scss
@@ -1,42 +1,52 @@
/* _wizard.scss */
-.quipucords-wizard, .wizard-pf {
- .wizard-pf-main {
- overflow: inherit;
+.quipucords-wizard {
+ @media (min-width: $pf-global--breakpoint--sm) {
+ flex: unset !important;
+ min-height: 37rem !important;
}
- abbr {
- border-bottom: none;
- }
+ &__hide-nav {
+ .pf-c-wizard__outer-wrap {
+ padding-left: 0;
- .form-horizontal .form-group {
- margin-left:0;
- margin-right:0;
+ nav {
+ display: none;
+ }
+ }
}
- .form-horizontal .form-group label {
- padding-right: 0;
+ &__hide-nav-last {
+ .pf-c-wizard__nav-list {
+ li:last-child {
+ display: none;
+ }
+ }
}
- .input-group .input-group-btn .btn {
- height: 26px;
- }
+ &__add-source {
+ abbr {
+ border-bottom: none;
+ }
- .form-horizontal h3 {
- margin-top: 0;
- }
+ .form-horizontal .form-group {
+ margin-left:0;
+ margin-right:0;
+ }
- .btn-file {
- @extend .input-group-btn;
- }
+ .form-horizontal .form-group label {
+ padding-right: 0;
+ }
- @media (min-width: $screen-sm-min) {
- margin: 30px auto;
- padding: 0;
- width: 600px;
+ .input-group .input-group-btn .btn {
+ height: 26px;
+ }
+
+ .form-horizontal h3 {
+ margin-top: 0;
+ }
- .wizard-pf-row {
- min-height: 450px;
- height: auto;
+ .btn-file {
+ @extend .input-group-btn;
}
}
}
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 5c0094f1..ea35ebd8 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -1,27 +1,39 @@
-
// Variables
-$font-path: '~patternfly/dist/fonts/';
-$img-path: '~patternfly/dist/img/';
+$font-path: '~patternfly/dist/fonts/';
+$img-path: '~patternfly/dist/img/';
$icon-font-path: '~patternfly/dist/fonts/';
+@import '~patternfly/dist/css/rcue.css';
+@import '~patternfly/dist/css/rcue-additions.css';
+@import '~@patternfly/react-core/dist/styles/base.css';
+
// Include patternfly variables before bootstrap variables
-@import 'patternfly/dist/sass/patternfly/variables';
+@import '~patternfly/dist/sass/patternfly/variables';
+@import '~@patternfly/patternfly/sass-utilities/all';
// Bootstrap Core variables and mixins
-@import 'bootstrap-sass/assets/stylesheets/bootstrap/variables';
-@import 'bootstrap-sass/assets/stylesheets/bootstrap/mixins';
+@import '~bootstrap-sass/assets/stylesheets/bootstrap/variables';
+@import '~bootstrap-sass/assets/stylesheets/bootstrap/mixins';
// Patternfly Core variables and mixins
-@import 'patternfly/dist/sass/patternfly/bootstrap-mixin-overrides';
-@import 'patternfly/dist/sass/patternfly/mixins';
+@import '~patternfly/dist/sass/patternfly/bootstrap-mixin-overrides';
+@import '~patternfly/dist/sass/patternfly/mixins';
+@import 'app/vars';
@import 'app/overrides';
@import 'app/common';
@import 'app/aboutModal';
+@import 'app/modal';
@import 'app/login';
@import 'app/app';
+@import 'app/dropdownSelect';
+@import 'app/table';
@import 'app/forms';
@import 'app/pageLayout';
@import 'app/scans';
+@import 'app/toastNotifications';
+@import 'app/tooltip';
@import 'app/views';
@import 'app/wizard';
+@import 'app/emptyState';
+@import 'app/patternflyOverrides';
diff --git a/src/styles/template.scss b/src/styles/template.scss
new file mode 100644
index 00000000..7b1abf99
--- /dev/null
+++ b/src/styles/template.scss
@@ -0,0 +1,34 @@
+/**
+ * FixMe: this file is temporary, styling within this file is used for Django templates
+ * When the templates are cleaned up, or removed, review if this file is still necessary
+ */
+// Variables
+$font-path: '~patternfly/dist/fonts/';
+$img-path: '~patternfly/dist/img/';
+$icon-font-path: '~patternfly/dist/fonts/';
+
+@use '@patternfly/react-core/dist/styles/base-no-reset.css';
+
+// Include patternfly variables before bootstrap variables
+@import 'patternfly/dist/sass/patternfly/variables';
+@import '@patternfly/patternfly/sass-utilities/all';
+
+// Bootstrap Core variables and mixins
+@import 'bootstrap-sass/assets/stylesheets/bootstrap/variables';
+@import 'bootstrap-sass/assets/stylesheets/bootstrap/mixins';
+
+// Patternfly Core variables and mixins
+@import 'patternfly/dist/sass/patternfly/bootstrap-mixin-overrides';
+@import 'patternfly/dist/sass/patternfly/mixins';
+
+@import 'app/vars';
+@import 'app/overrides';
+@import 'app/common';
+// @import 'app/aboutModal';
+@import 'app/login';
+@import 'app/app';
+@import 'app/forms';
+@import 'app/pageLayout';
+// @import 'app/scans';
+@import 'app/views';
+// @import 'app/wizard';
diff --git a/tests/__snapshots__/code.test.js.snap b/tests/__snapshots__/code.test.js.snap
index ae43518e..759a3a91 100644
--- a/tests/__snapshots__/code.test.js.snap
+++ b/tests/__snapshots__/code.test.js.snap
@@ -4,8 +4,8 @@ exports[`General code checks should only have specific console.[warn|log|info|er
Array [
"common/helpers.js:63: console.warn('Copy to clipboard failed.', e.message);",
"common/helpers.js:222: console.error(\`Unknown status: \${scanStatus}\`);",
- "redux/common/reduxHelpers.js:14: console.error(\`Error: Property \${prop} does not exist within the passed state.\`, state);",
- "redux/common/reduxHelpers.js:18: console.warn(\`Warning: Property \${prop} does not exist within the passed initialState.\`, initialState);",
- "setupTests.js:151: console.error = (message, ...args) => {",
+ "redux/common/reduxHelpers.js:15: console.error(\`Error: Property \${prop} does not exist within the passed state.\`, state);",
+ "redux/common/reduxHelpers.js:19: console.warn(\`Warning: Property \${prop} does not exist within the passed initialState.\`, initialState);",
+ "setupTests.js:159: console.error = (message, ...args) => {",
]
`;
diff --git a/tests/__snapshots__/dist.test.js.snap b/tests/__snapshots__/dist.test.js.snap
index bc55c80c..10da66c1 100644
--- a/tests/__snapshots__/dist.test.js.snap
+++ b/tests/__snapshots__/dist.test.js.snap
@@ -570,13 +570,181 @@ Array [
"./dist/client/locales/en.json",
"./dist/client/locales/locales.json",
"./dist/client/manifest.json",
+ "./dist/client/static/css/10*chunk*map",
+ "./dist/client/static/css/10*chunk.css",
+ "./dist/client/static/css/11*chunk*map",
+ "./dist/client/static/css/11*chunk.css",
+ "./dist/client/static/css/12*chunk*map",
+ "./dist/client/static/css/12*chunk.css",
+ "./dist/client/static/css/13*chunk*map",
+ "./dist/client/static/css/13*chunk.css",
+ "./dist/client/static/css/14*chunk*map",
+ "./dist/client/static/css/14*chunk.css",
+ "./dist/client/static/css/15*chunk*map",
+ "./dist/client/static/css/15*chunk.css",
+ "./dist/client/static/css/16*chunk*map",
+ "./dist/client/static/css/16*chunk.css",
+ "./dist/client/static/css/17*chunk*map",
+ "./dist/client/static/css/17*chunk.css",
+ "./dist/client/static/css/18*chunk*map",
+ "./dist/client/static/css/18*chunk.css",
+ "./dist/client/static/css/19*chunk*map",
+ "./dist/client/static/css/19*chunk.css",
"./dist/client/static/css/2*chunk*map",
"./dist/client/static/css/2*chunk.css",
+ "./dist/client/static/css/20*chunk*map",
+ "./dist/client/static/css/20*chunk.css",
+ "./dist/client/static/css/21*chunk*map",
+ "./dist/client/static/css/21*chunk.css",
+ "./dist/client/static/css/22*chunk*map",
+ "./dist/client/static/css/22*chunk.css",
+ "./dist/client/static/css/23*chunk*map",
+ "./dist/client/static/css/23*chunk.css",
+ "./dist/client/static/css/24*chunk*map",
+ "./dist/client/static/css/24*chunk.css",
+ "./dist/client/static/css/25*chunk*map",
+ "./dist/client/static/css/25*chunk.css",
+ "./dist/client/static/css/26*chunk*map",
+ "./dist/client/static/css/26*chunk.css",
+ "./dist/client/static/css/27*chunk*map",
+ "./dist/client/static/css/27*chunk.css",
+ "./dist/client/static/css/28*chunk*map",
+ "./dist/client/static/css/28*chunk.css",
+ "./dist/client/static/css/29*chunk*map",
+ "./dist/client/static/css/29*chunk.css",
+ "./dist/client/static/css/3*chunk*map",
+ "./dist/client/static/css/3*chunk.css",
+ "./dist/client/static/css/30*chunk*map",
+ "./dist/client/static/css/30*chunk.css",
+ "./dist/client/static/css/31*chunk*map",
+ "./dist/client/static/css/31*chunk.css",
+ "./dist/client/static/css/32*chunk*map",
+ "./dist/client/static/css/32*chunk.css",
+ "./dist/client/static/css/33*chunk*map",
+ "./dist/client/static/css/33*chunk.css",
+ "./dist/client/static/css/34*chunk*map",
+ "./dist/client/static/css/34*chunk.css",
+ "./dist/client/static/css/35*chunk*map",
+ "./dist/client/static/css/35*chunk.css",
+ "./dist/client/static/css/36*chunk*map",
+ "./dist/client/static/css/36*chunk.css",
+ "./dist/client/static/css/37*chunk*map",
+ "./dist/client/static/css/37*chunk.css",
+ "./dist/client/static/css/38*chunk*map",
+ "./dist/client/static/css/38*chunk.css",
+ "./dist/client/static/css/39*chunk*map",
+ "./dist/client/static/css/39*chunk.css",
+ "./dist/client/static/css/4*chunk*map",
+ "./dist/client/static/css/4*chunk.css",
+ "./dist/client/static/css/40*chunk*map",
+ "./dist/client/static/css/40*chunk.css",
+ "./dist/client/static/css/41*chunk*map",
+ "./dist/client/static/css/41*chunk.css",
+ "./dist/client/static/css/42*chunk*map",
+ "./dist/client/static/css/42*chunk.css",
+ "./dist/client/static/css/43*chunk*map",
+ "./dist/client/static/css/43*chunk.css",
+ "./dist/client/static/css/44*chunk*map",
+ "./dist/client/static/css/44*chunk.css",
+ "./dist/client/static/css/5*chunk*map",
+ "./dist/client/static/css/5*chunk.css",
+ "./dist/client/static/css/6*chunk*map",
+ "./dist/client/static/css/6*chunk.css",
+ "./dist/client/static/css/7*chunk*map",
+ "./dist/client/static/css/7*chunk.css",
+ "./dist/client/static/css/8*chunk*map",
+ "./dist/client/static/css/8*chunk.css",
+ "./dist/client/static/css/9*chunk*map",
+ "./dist/client/static/css/9*chunk.css",
"./dist/client/static/css/main*chunk*map",
"./dist/client/static/css/main*chunk.css",
+ "./dist/client/static/js/10*chunk*map",
+ "./dist/client/static/js/10*chunk.js",
+ "./dist/client/static/js/11*chunk*map",
+ "./dist/client/static/js/11*chunk.js",
+ "./dist/client/static/js/12*chunk*map",
+ "./dist/client/static/js/12*chunk.js",
+ "./dist/client/static/js/13*chunk*map",
+ "./dist/client/static/js/13*chunk.js",
+ "./dist/client/static/js/14*chunk*map",
+ "./dist/client/static/js/14*chunk.js",
+ "./dist/client/static/js/15*chunk*map",
+ "./dist/client/static/js/15*chunk.js",
+ "./dist/client/static/js/16*chunk*map",
+ "./dist/client/static/js/16*chunk.js",
+ "./dist/client/static/js/17*chunk*map",
+ "./dist/client/static/js/17*chunk.js",
+ "./dist/client/static/js/18*chunk*map",
+ "./dist/client/static/js/18*chunk.js",
+ "./dist/client/static/js/19*chunk*map",
+ "./dist/client/static/js/19*chunk.js",
"./dist/client/static/js/2*chunk*LICENSE.txt",
"./dist/client/static/js/2*chunk*map",
"./dist/client/static/js/2*chunk.js",
+ "./dist/client/static/js/20*chunk*map",
+ "./dist/client/static/js/20*chunk.js",
+ "./dist/client/static/js/21*chunk*map",
+ "./dist/client/static/js/21*chunk.js",
+ "./dist/client/static/js/22*chunk*map",
+ "./dist/client/static/js/22*chunk.js",
+ "./dist/client/static/js/23*chunk*map",
+ "./dist/client/static/js/23*chunk.js",
+ "./dist/client/static/js/24*chunk*map",
+ "./dist/client/static/js/24*chunk.js",
+ "./dist/client/static/js/25*chunk*map",
+ "./dist/client/static/js/25*chunk.js",
+ "./dist/client/static/js/26*chunk*map",
+ "./dist/client/static/js/26*chunk.js",
+ "./dist/client/static/js/27*chunk*map",
+ "./dist/client/static/js/27*chunk.js",
+ "./dist/client/static/js/28*chunk*map",
+ "./dist/client/static/js/28*chunk.js",
+ "./dist/client/static/js/29*chunk*map",
+ "./dist/client/static/js/29*chunk.js",
+ "./dist/client/static/js/3*chunk*map",
+ "./dist/client/static/js/3*chunk.js",
+ "./dist/client/static/js/30*chunk*map",
+ "./dist/client/static/js/30*chunk.js",
+ "./dist/client/static/js/31*chunk*map",
+ "./dist/client/static/js/31*chunk.js",
+ "./dist/client/static/js/32*chunk*map",
+ "./dist/client/static/js/32*chunk.js",
+ "./dist/client/static/js/33*chunk*map",
+ "./dist/client/static/js/33*chunk.js",
+ "./dist/client/static/js/34*chunk*map",
+ "./dist/client/static/js/34*chunk.js",
+ "./dist/client/static/js/35*chunk*map",
+ "./dist/client/static/js/35*chunk.js",
+ "./dist/client/static/js/36*chunk*map",
+ "./dist/client/static/js/36*chunk.js",
+ "./dist/client/static/js/37*chunk*map",
+ "./dist/client/static/js/37*chunk.js",
+ "./dist/client/static/js/38*chunk*map",
+ "./dist/client/static/js/38*chunk.js",
+ "./dist/client/static/js/39*chunk*map",
+ "./dist/client/static/js/39*chunk.js",
+ "./dist/client/static/js/4*chunk*map",
+ "./dist/client/static/js/4*chunk.js",
+ "./dist/client/static/js/40*chunk*map",
+ "./dist/client/static/js/40*chunk.js",
+ "./dist/client/static/js/41*chunk*map",
+ "./dist/client/static/js/41*chunk.js",
+ "./dist/client/static/js/42*chunk*map",
+ "./dist/client/static/js/42*chunk.js",
+ "./dist/client/static/js/43*chunk*map",
+ "./dist/client/static/js/43*chunk.js",
+ "./dist/client/static/js/44*chunk*map",
+ "./dist/client/static/js/44*chunk.js",
+ "./dist/client/static/js/5*chunk*map",
+ "./dist/client/static/js/5*chunk.js",
+ "./dist/client/static/js/6*chunk*map",
+ "./dist/client/static/js/6*chunk.js",
+ "./dist/client/static/js/7*chunk*map",
+ "./dist/client/static/js/7*chunk.js",
+ "./dist/client/static/js/8*chunk*map",
+ "./dist/client/static/js/8*chunk.js",
+ "./dist/client/static/js/9*chunk*map",
+ "./dist/client/static/js/9*chunk.js",
"./dist/client/static/js/main*chunk*map",
"./dist/client/static/js/main*chunk.js",
"./dist/client/static/js/runtime-main*js",
@@ -635,7 +803,28 @@ Array [
"./dist/client/static/media/PatternFlyIcons-webfont*svg",
"./dist/client/static/media/PatternFlyIcons-webfont*ttf",
"./dist/client/static/media/PatternFlyIcons-webfont*woff",
- "./dist/client/static/media/about-bg*png",
+ "./dist/client/static/media/RedHatDisplay-Bold*woff",
+ "./dist/client/static/media/RedHatDisplay-Bold*woff2",
+ "./dist/client/static/media/RedHatDisplay-Medium*woff",
+ "./dist/client/static/media/RedHatDisplay-Medium*woff2",
+ "./dist/client/static/media/RedHatDisplay-Regular*woff",
+ "./dist/client/static/media/RedHatDisplay-Regular*woff2",
+ "./dist/client/static/media/RedHatDisplay-updated-Bold*woff2",
+ "./dist/client/static/media/RedHatDisplay-updated-Medium*woff2",
+ "./dist/client/static/media/RedHatDisplay-updated-Regular*woff2",
+ "./dist/client/static/media/RedHatDisplayVF-updated-ItalicModified*woff2",
+ "./dist/client/static/media/RedHatDisplayVFModified-updated*woff2",
+ "./dist/client/static/media/RedHatMono-updated-Regular*woff2",
+ "./dist/client/static/media/RedHatMonoVF-updated*woff2",
+ "./dist/client/static/media/RedHatMonoVF-updated-Italic*woff2",
+ "./dist/client/static/media/RedHatText-Medium*woff",
+ "./dist/client/static/media/RedHatText-Medium*woff2",
+ "./dist/client/static/media/RedHatText-Regular*woff",
+ "./dist/client/static/media/RedHatText-Regular*woff2",
+ "./dist/client/static/media/RedHatText-updated-Medium*woff2",
+ "./dist/client/static/media/RedHatText-updated-Regular*woff2",
+ "./dist/client/static/media/RedHatTextVF-updated-ItalicModified*woff2",
+ "./dist/client/static/media/RedHatTextVFModified-updated*woff2",
"./dist/client/static/media/fontawesome-webfont*eot",
"./dist/client/static/media/fontawesome-webfont*svg",
"./dist/client/static/media/fontawesome-webfont*ttf",
@@ -648,6 +837,55 @@ Array [
"./dist/client/static/media/glyphicons-halflings-regular*woff2",
"./dist/client/static/media/logo*svg",
"./dist/client/static/media/logo-brand*svg",
+ "./dist/client/static/media/overpass-bold*woff",
+ "./dist/client/static/media/overpass-bold*woff2",
+ "./dist/client/static/media/overpass-bold-italic*woff",
+ "./dist/client/static/media/overpass-bold-italic*woff2",
+ "./dist/client/static/media/overpass-extrabold*woff",
+ "./dist/client/static/media/overpass-extrabold*woff2",
+ "./dist/client/static/media/overpass-extrabold-italic*woff",
+ "./dist/client/static/media/overpass-extrabold-italic*woff2",
+ "./dist/client/static/media/overpass-extralight*woff",
+ "./dist/client/static/media/overpass-extralight*woff2",
+ "./dist/client/static/media/overpass-extralight-italic*woff",
+ "./dist/client/static/media/overpass-extralight-italic*woff2",
+ "./dist/client/static/media/overpass-heavy*woff",
+ "./dist/client/static/media/overpass-heavy*woff2",
+ "./dist/client/static/media/overpass-heavy-italic*woff",
+ "./dist/client/static/media/overpass-heavy-italic*woff2",
+ "./dist/client/static/media/overpass-italic*woff",
+ "./dist/client/static/media/overpass-italic*woff2",
+ "./dist/client/static/media/overpass-light*woff",
+ "./dist/client/static/media/overpass-light*woff2",
+ "./dist/client/static/media/overpass-light-italic*woff",
+ "./dist/client/static/media/overpass-light-italic*woff2",
+ "./dist/client/static/media/overpass-mono-bold*woff",
+ "./dist/client/static/media/overpass-mono-bold*woff2",
+ "./dist/client/static/media/overpass-mono-light*woff",
+ "./dist/client/static/media/overpass-mono-light*woff2",
+ "./dist/client/static/media/overpass-mono-regular*woff",
+ "./dist/client/static/media/overpass-mono-regular*woff2",
+ "./dist/client/static/media/overpass-mono-semibold*woff",
+ "./dist/client/static/media/overpass-mono-semibold*woff2",
+ "./dist/client/static/media/overpass-regular*woff",
+ "./dist/client/static/media/overpass-regular*woff2",
+ "./dist/client/static/media/overpass-semibold*woff",
+ "./dist/client/static/media/overpass-semibold*woff2",
+ "./dist/client/static/media/overpass-semibold-italic*woff",
+ "./dist/client/static/media/overpass-semibold-italic*woff2",
+ "./dist/client/static/media/overpass-thin*woff",
+ "./dist/client/static/media/overpass-thin*woff2",
+ "./dist/client/static/media/overpass-thin-italic*woff",
+ "./dist/client/static/media/overpass-thin-italic*woff2",
+ "./dist/client/static/media/pfbg_2000*jpg",
+ "./dist/client/static/media/pfbg_576*jpg",
+ "./dist/client/static/media/pfbg_576@2x*jpg",
+ "./dist/client/static/media/pfbg_768*jpg",
+ "./dist/client/static/media/pfbg_768@2x*jpg",
+ "./dist/client/static/media/pfbg_992@2x*jpg",
+ "./dist/client/static/media/pficon*woff",
+ "./dist/client/static/media/pficon*woff2",
+ "./dist/client/static/media/status-icon-sprite*svg",
"./dist/client/static/media/title*svg",
"./dist/client/static/media/title-brand*svg",
"./dist/templates/base.html",
diff --git a/tests/__snapshots__/i18n.test.js.snap b/tests/__snapshots__/i18n.test.js.snap
deleted file mode 100644
index 8deb9356..00000000
--- a/tests/__snapshots__/i18n.test.js.snap
+++ /dev/null
@@ -1,33 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`i18n locale should generate a predictable pot output snapshot: pot output 1`] = `
-"msgid \\"\\"
-msgstr \\"\\"
-\\"Content-Type: text/plain; charset=UTF-8\\\\n\\"
-
-#: src/components/aboutModal/aboutModal.js:99
-msgctxt \\"Browser OS\\"
-msgid \\"about-modal.browser-os\\"
-msgstr \\"\\"
-
-#: src/components/aboutModal/aboutModal.js:93
-msgctxt \\"Browser Version\\"
-msgid \\"about-modal.browser-version\\"
-msgstr \\"\\"
-
-#: src/components/aboutModal/aboutModal.js:105
-msgctxt \\"Server Version\\"
-msgid \\"about-modal.server-version\\"
-msgstr \\"\\"
-
-#: src/components/aboutModal/aboutModal.js:110
-msgctxt \\"UI Version\\"
-msgid \\"about-modal.ui-version\\"
-msgstr \\"\\"
-
-#: src/components/aboutModal/aboutModal.js:89
-msgctxt \\"Username\\"
-msgid \\"about-modal.username\\"
-msgstr \\"\\"
-"
-`;
diff --git a/tests/i18n.test.js b/tests/i18n.test.js
deleted file mode 100644
index 0ea18823..00000000
--- a/tests/i18n.test.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const { GettextExtractor, JsExtractors } = require('gettext-extractor');
-
-const textExtractor = () => {
- const extractor = new GettextExtractor();
- extractor
- .createJsParser([
- JsExtractors.callExpression(['t', '[this].t'], {
- arguments: {
- text: 0,
- context: 1
- }
- })
- ])
- .parseFilesGlob('./src/components/**/*!(.test|.spec).@(js|jsx)');
-
- return extractor;
-};
-
-describe('i18n locale', () => {
- const getText = textExtractor();
-
- it('should generate a predictable pot output snapshot', () => {
- expect(getText.getPotString()).toMatchSnapshot('pot output');
- });
-});
diff --git a/yarn.lock b/yarn.lock
index cd5d133d..e46ae9d8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1601,6 +1601,79 @@
node-gyp "^9.0.0"
read-package-json-fast "^2.0.3"
+"@patternfly/patternfly@4.196.7":
+ version "4.196.7"
+ resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-4.196.7.tgz#5f39c53e0a6a626ea6e3abfd5bcc2e3695818ceb"
+ integrity sha512-hA7Oww411e1p0/IXjC1I+4/1NNis9V+NVBxfUIpRwyuLbCIDHBdtMu2qAPLdKxXjuibV9EE6ZdlT7ra/kcFuJQ==
+
+"@patternfly/react-core@4.221.3":
+ version "4.221.3"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-4.221.3.tgz#7007be54b37a044ac78ba4243614043e8da7be24"
+ integrity sha512-I33TnX5Xn8ypXYjHc7G5kIVsYjB4PnDxxmJG6rBLqFslrnGUBzvb0sflnP43QlI0camamVIoaBRjedj8WXqaRg==
+ dependencies:
+ "@patternfly/react-icons" "^4.72.3"
+ "@patternfly/react-styles" "^4.71.3"
+ "@patternfly/react-tokens" "^4.73.3"
+ focus-trap "6.9.2"
+ react-dropzone "9.0.0"
+ tippy.js "5.1.2"
+ tslib "^2.0.0"
+
+"@patternfly/react-core@^4.202.37":
+ version "4.224.1"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-4.224.1.tgz#d8d81e7cf611fd441f9fb970db8e0c3a36fa508b"
+ integrity sha512-v8wGGNoMGndAScAoE5jeOA5jVgymlLSwttPjQk/Idr0k7roSpOrsM39oXUR5DEgkZee45DW00WKTgmg50PP3FQ==
+ dependencies:
+ "@patternfly/react-icons" "^4.75.1"
+ "@patternfly/react-styles" "^4.74.1"
+ "@patternfly/react-tokens" "^4.76.1"
+ focus-trap "6.9.2"
+ react-dropzone "9.0.0"
+ tippy.js "5.1.2"
+ tslib "^2.0.0"
+
+"@patternfly/react-icons@4.72.3", "@patternfly/react-icons@^4.72.3":
+ version "4.72.3"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.72.3.tgz#5886755a2b516d49d97ed3bdc88609d9b3c4fc2a"
+ integrity sha512-bZCPsOsxtFXTmZQqDKNebkBywud3E0ID3446AWI1RO5Ypufdc0FTkehSzBPANfJPYjjK9/EYaIy8rF0yiJdFPQ==
+
+"@patternfly/react-icons@^4.53.37", "@patternfly/react-icons@^4.75.1":
+ version "4.75.1"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.75.1.tgz#3567b5a21a7f52c6a272f0330357fac87083867a"
+ integrity sha512-1ly8SVi/kcc0zkiViOjUd8D5BEr7GeqWGmDPuDSBtD60l1dYf3hZc44IWFVkRM/oHZML/musdrJkLfh4MDqX9w==
+
+"@patternfly/react-styles@4.71.3", "@patternfly/react-styles@^4.71.3":
+ version "4.71.3"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.71.3.tgz#4f1cf4624a675751f035dc3bfa4cb3c6ca48e43b"
+ integrity sha512-JpMBIrJfco3JwK9KbJvjr+tKZidVhbkGrQT7GyWbYAwZ2bOmCmeCPJYc0xhAWcvNumn9a7AhYzAWPJVlQQue3g==
+
+"@patternfly/react-styles@^4.52.37", "@patternfly/react-styles@^4.74.1":
+ version "4.74.1"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.74.1.tgz#3cd19cd31dd896bfd046f79241e8a8aefbfb1152"
+ integrity sha512-9eWvKrjtrJ3qhJkhY2GQKyYA13u/J0mU1befH49SYbvxZtkbuHdpKmXBAeQoHmcx1hcOKtiYXeKb+dVoRRNx0A==
+
+"@patternfly/react-table@4.71.37":
+ version "4.71.37"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-4.71.37.tgz#32c4ae15d66f8402d371123b7419045a2da0e362"
+ integrity sha512-YeB+gfl28ak3LPz3y5vTXe66PEUO7xIZyAAyngSnIeyzxCJh8l6RZlCKGMIY35SnEEbvwiyLgmk5ZzrLsIClGg==
+ dependencies:
+ "@patternfly/react-core" "^4.202.37"
+ "@patternfly/react-icons" "^4.53.37"
+ "@patternfly/react-styles" "^4.52.37"
+ "@patternfly/react-tokens" "^4.54.37"
+ lodash "^4.17.19"
+ tslib "^2.0.0"
+
+"@patternfly/react-tokens@4.73.3", "@patternfly/react-tokens@^4.73.3":
+ version "4.73.3"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-4.73.3.tgz#dae690220c25c242d2c45ec571e46d8cdcc7de13"
+ integrity sha512-WyEcV9jiMZzscQMBhlDkypHw9qg0wbX7r/fe8HcWws+jnYWGVjwUdnr18ktI9aw/h/oQS46sirf8xbNTlIiQFg==
+
+"@patternfly/react-tokens@^4.54.37", "@patternfly/react-tokens@^4.76.1":
+ version "4.76.1"
+ resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-4.76.1.tgz#ed85c3f6c6e779398579467e566d6750d01e8319"
+ integrity sha512-gLEezRSzQeflaPu3SCgYmWtuiqDIRtxNNFP1+ES7P2o56YHXJ5o1Pki7LpNCPk/VOzHy2+vRFE/7l+hBEweugw==
+
"@pmmmwh/react-refresh-webpack-plugin@0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz#1eec460596d200c0236bf195b078a5d1df89b766"
@@ -2094,25 +2167,11 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
-"@types/events@*":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
- integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
-
"@types/geojson@*":
version "7946.0.7"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"
integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==
-"@types/glob@5 - 7":
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
- integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
- dependencies:
- "@types/events" "*"
- "@types/minimatch" "*"
- "@types/node" "*"
-
"@types/glob@^7.1.1":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
@@ -2205,11 +2264,6 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
-"@types/parse5@^5":
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.0.tgz#9ae2106efc443d7c1e26570aa8247828c9c80f11"
- integrity sha512-J5D3z703XTDIGQFYXsnU9uRCW9e9mMEFO0Kpe6kykyiboqziru/RlZ0hM2P+PKTG4NHG1SjLrqae/NrV2iJApQ==
-
"@types/prettier@^2.0.0":
version "2.6.3"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.3.tgz#68ada76827b0010d0db071f739314fa429943d0a"
@@ -3011,15 +3065,15 @@ array-ify@^1.0.0:
resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece"
integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=
-array-includes@^3.1.4, array-includes@^3.1.5:
- version "3.1.5"
- resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.5.tgz#2c320010db8d31031fd2a5f6b3bbd4b1aad31bdb"
- integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==
+array-includes@^3.1.4, array-includes@^3.1.6:
+ version "3.1.6"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f"
+ integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
- es-abstract "^1.19.5"
- get-intrinsic "^1.1.1"
+ es-abstract "^1.20.4"
+ get-intrinsic "^1.1.3"
is-string "^1.0.7"
array-map@~0.0.0:
@@ -3072,14 +3126,14 @@ array.prototype.flat@^1.2.5:
es-abstract "^1.19.2"
es-shim-unscopables "^1.0.0"
-array.prototype.flatmap@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz#a7e8ed4225f4788a70cd910abcf0791e76a5534f"
- integrity sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==
+array.prototype.flatmap@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183"
+ integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
es-shim-unscopables "^1.0.0"
array.prototype.reduce@^1.0.4:
@@ -3093,6 +3147,17 @@ array.prototype.reduce@^1.0.4:
es-array-method-boxes-properly "^1.0.0"
is-string "^1.0.7"
+array.prototype.tosorted@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532"
+ integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+ es-shim-unscopables "^1.0.0"
+ get-intrinsic "^1.1.3"
+
arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
@@ -3177,6 +3242,13 @@ atob@^2.1.1, atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+attr-accept@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
+ integrity sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==
+ dependencies:
+ core-js "^2.5.0"
+
autoprefixer@^9.6.1:
version "9.8.8"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a"
@@ -3190,6 +3262,11 @@ autoprefixer@^9.6.1:
postcss "^7.0.32"
postcss-value-parser "^4.1.0"
+available-typed-arrays@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
+ integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
+
axe-core@^4.3.5:
version "4.4.2"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.2.tgz#dcf7fb6dea866166c3eab33d68208afe4d5f670c"
@@ -4667,6 +4744,11 @@ core-js@^2.4.0, core-js@^2.6.5:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895"
integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==
+core-js@^2.5.0:
+ version "2.6.12"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
+ integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
+
core-js@^3.6.5:
version "3.22.8"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.22.8.tgz#23f860b1fe60797cc4f704d76c93fea8a2f60631"
@@ -4894,11 +4976,6 @@ css-select@~1.2.0:
domutils "1.5.1"
nth-check "~1.0.1"
-css-selector-parser@^1.3:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.3.0.tgz#5f1ad43e2d8eefbfdc304fcd39a521664943e3eb"
- integrity sha1-XxrUPi2O77/cME/NOaUhZklD4+s=
-
css-tree@1.0.0-alpha.37:
version "1.0.0-alpha.37"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22"
@@ -5815,18 +5892,6 @@ error-stack-parser@^2.0.6:
dependencies:
stackframe "^1.3.4"
-es-abstract@^1.12.0, es-abstract@^1.4.3, es-abstract@^1.5.1:
- version "1.13.0"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
- integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
- dependencies:
- es-to-primitive "^1.2.0"
- function-bind "^1.1.1"
- has "^1.0.3"
- is-callable "^1.1.4"
- is-regex "^1.0.4"
- object-keys "^1.0.12"
-
es-abstract@^1.17.0-next.1:
version "1.17.2"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.2.tgz#965b10af56597b631da15872c17a405e86c1fd46"
@@ -5844,7 +5909,7 @@ es-abstract@^1.17.0-next.1:
string.prototype.trimleft "^2.1.1"
string.prototype.trimright "^2.1.1"
-es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.1:
+es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814"
integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==
@@ -5873,11 +5938,71 @@ es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19
string.prototype.trimstart "^1.0.5"
unbox-primitive "^1.0.2"
+es-abstract@^1.20.4:
+ version "1.21.1"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6"
+ integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg==
+ dependencies:
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ es-set-tostringtag "^2.0.1"
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ function.prototype.name "^1.1.5"
+ get-intrinsic "^1.1.3"
+ get-symbol-description "^1.0.0"
+ globalthis "^1.0.3"
+ gopd "^1.0.1"
+ has "^1.0.3"
+ has-property-descriptors "^1.0.0"
+ has-proto "^1.0.1"
+ has-symbols "^1.0.3"
+ internal-slot "^1.0.4"
+ is-array-buffer "^3.0.1"
+ is-callable "^1.2.7"
+ is-negative-zero "^2.0.2"
+ is-regex "^1.1.4"
+ is-shared-array-buffer "^1.0.2"
+ is-string "^1.0.7"
+ is-typed-array "^1.1.10"
+ is-weakref "^1.0.2"
+ object-inspect "^1.12.2"
+ object-keys "^1.1.1"
+ object.assign "^4.1.4"
+ regexp.prototype.flags "^1.4.3"
+ safe-regex-test "^1.0.0"
+ string.prototype.trimend "^1.0.6"
+ string.prototype.trimstart "^1.0.6"
+ typed-array-length "^1.0.4"
+ unbox-primitive "^1.0.2"
+ which-typed-array "^1.1.9"
+
+es-abstract@^1.4.3, es-abstract@^1.5.1:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
+ integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
+ dependencies:
+ es-to-primitive "^1.2.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ is-callable "^1.1.4"
+ is-regex "^1.0.4"
+ object-keys "^1.0.12"
+
es-array-method-boxes-properly@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==
+es-set-tostringtag@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8"
+ integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==
+ dependencies:
+ get-intrinsic "^1.1.3"
+ has "^1.0.3"
+ has-tostringtag "^1.0.0"
+
es-shim-unscopables@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241"
@@ -6114,25 +6239,26 @@ eslint-plugin-react-hooks@^4.6.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==
-eslint-plugin-react@^7.21.5, eslint-plugin-react@^7.30.0:
- version "7.30.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.30.0.tgz#8e7b1b2934b8426ac067a0febade1b13bd7064e3"
- integrity sha512-RgwH7hjW48BleKsYyHK5vUAvxtE9SMPDKmcPRQgtRCYaZA0XQPt5FSkrU3nhz5ifzMZcA8opwmRJ2cmOO8tr5A==
+eslint-plugin-react@^7.21.5, eslint-plugin-react@^7.32.2:
+ version "7.32.2"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz#e71f21c7c265ebce01bcbc9d0955170c55571f10"
+ integrity sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==
dependencies:
- array-includes "^3.1.5"
- array.prototype.flatmap "^1.3.0"
+ array-includes "^3.1.6"
+ array.prototype.flatmap "^1.3.1"
+ array.prototype.tosorted "^1.1.1"
doctrine "^2.1.0"
estraverse "^5.3.0"
jsx-ast-utils "^2.4.1 || ^3.0.0"
minimatch "^3.1.2"
- object.entries "^1.1.5"
- object.fromentries "^2.0.5"
- object.hasown "^1.1.1"
- object.values "^1.1.5"
+ object.entries "^1.1.6"
+ object.fromentries "^2.0.6"
+ object.hasown "^1.1.2"
+ object.values "^1.1.6"
prop-types "^15.8.1"
- resolve "^2.0.0-next.3"
+ resolve "^2.0.0-next.4"
semver "^6.3.0"
- string.prototype.matchall "^4.0.7"
+ string.prototype.matchall "^4.0.8"
eslint-plugin-testing-library@^3.9.2:
version "3.10.2"
@@ -6624,6 +6750,13 @@ file-loader@6.1.1:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
+file-selector@^0.1.8:
+ version "0.1.19"
+ resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.19.tgz#8ecc9d069a6f544f2e4a096b64a8052e70ec8abf"
+ integrity sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==
+ dependencies:
+ tslib "^2.0.1"
+
filesize@6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-6.1.0.tgz#e81bdaa780e2451d714d71c0d7a4f3238d37ad00"
@@ -6751,6 +6884,13 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
+focus-trap@6.9.2:
+ version "6.9.2"
+ resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-6.9.2.tgz#a9ef72847869bd2cbf62cb930aaf8e138fef1ca9"
+ integrity sha512-gBEuXOPNOKPrLdZpMFUSTyIo1eT2NSZRrwZ9r/0Jqw5tmT3Yvxfmu8KBHw8xW2XQkw6E/JoG+OlEq7UDtSUNgw==
+ dependencies:
+ tabbable "^5.3.2"
+
follow-redirects@^1.0.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76"
@@ -6773,6 +6913,13 @@ font-awesome@^4.7.0:
resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"
integrity sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=
+for-each@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
+ integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
+ dependencies:
+ is-callable "^1.1.3"
+
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -7000,6 +7147,15 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
has "^1.0.3"
has-symbols "^1.0.1"
+get-intrinsic@^1.1.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
+ integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==
+ dependencies:
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.3"
+
get-own-enumerable-property-symbols@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
@@ -7052,19 +7208,6 @@ get-value@^2.0.3, get-value@^2.0.6:
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
-gettext-extractor@^3.5.4:
- version "3.5.4"
- resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.5.4.tgz#bd36c65b4d26014ffd925f9ac7b4738d6893d6b2"
- integrity sha512-iK4tSnteSw+pFMts43OP8hUnsOklbkxz3ytWqru7dPf8Ec3uzTYv1aw70ojAvKItmofpj1ibfY7sZWsdSN6zIw==
- dependencies:
- "@types/glob" "5 - 7"
- "@types/parse5" "^5"
- css-selector-parser "^1.3"
- glob "5 - 7"
- parse5 "5 - 6"
- pofile "1.0.x"
- typescript "2 - 4"
-
git-raw-commits@^2.0.8:
version "2.0.11"
resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz#bc3576638071d18655e1cc60d7f524920008d723"
@@ -7121,7 +7264,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
dependencies:
is-glob "^4.0.1"
-"glob@5 - 7", glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
+glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3:
version "7.1.4"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
@@ -7203,6 +7346,13 @@ globals@^12.1.0:
dependencies:
type-fest "^0.8.1"
+globalthis@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf"
+ integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==
+ dependencies:
+ define-properties "^1.1.3"
+
globby@11.0.1:
version "11.0.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357"
@@ -7243,6 +7393,13 @@ google-code-prettify@~1.0.5:
resolved "https://registry.yarnpkg.com/google-code-prettify/-/google-code-prettify-1.0.5.tgz#9f477f224dbfa62372e5ef803a7e157410400084"
integrity sha1-n0d/Ik2/piNy5e+AOn4VdBBAAIQ=
+gopd@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
+ integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+ dependencies:
+ get-intrinsic "^1.1.3"
+
got@^9.6.0:
version "9.6.0"
resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
@@ -7337,6 +7494,11 @@ has-property-descriptors@^1.0.0:
dependencies:
get-intrinsic "^1.1.1"
+has-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
+ integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
+
has-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
@@ -7963,6 +8125,15 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
+internal-slot@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3"
+ integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==
+ dependencies:
+ get-intrinsic "^1.1.3"
+ has "^1.0.3"
+ side-channel "^1.0.4"
+
invariant@^2.2.1, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -8014,6 +8185,15 @@ is-accessor-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
+is-array-buffer@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.1.tgz#deb1db4fcae48308d54ef2442706c0393997052a"
+ integrity sha512-ASfLknmY8Xa2XtB4wmbz13Wu202baeA18cJBCeCy0wXUHZF0IPyVEXqKEcd+t2fNSLLL1vC6k7lxZEojNbISXQ==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.3"
+ is-typed-array "^1.1.10"
+
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -8063,6 +8243,11 @@ is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+is-callable@^1.1.3, is-callable@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
+ integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
+
is-callable@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
@@ -8097,13 +8282,20 @@ is-color-stop@^1.0.0:
rgb-regex "^1.0.1"
rgba-regex "^1.0.0"
-is-core-module@^2.0.0, is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.1:
+is-core-module@^2.0.0, is-core-module@^2.5.0, is-core-module@^2.8.1:
version "2.9.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
dependencies:
has "^1.0.3"
+is-core-module@^2.9.0:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
+ integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
+ dependencies:
+ has "^1.0.3"
+
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -8399,6 +8591,17 @@ is-text-path@^1.0.1:
dependencies:
text-extensions "^1.0.0"
+is-typed-array@^1.1.10, is-typed-array@^1.1.9:
+ version "1.1.10"
+ resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f"
+ integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==
+ dependencies:
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-tostringtag "^1.0.0"
+
is-typedarray@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -9165,7 +9368,7 @@ json-stable-stringify-without-jsonify@^1.0.1:
json-stringify-safe@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
- integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+ integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
json5@^1.0.1:
version "1.0.1"
@@ -9860,14 +10063,14 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
-minimatch@3.0.4, minimatch@^3.0.4:
+minimatch@3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
-minimatch@^3.1.1, minimatch@^3.1.2:
+minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -10037,15 +10240,10 @@ moment-timezone@^0.4.0, moment-timezone@^0.4.1:
dependencies:
moment ">= 2.6.0"
-"moment@>= 2.6.0", moment@^2.10, moment@^2.19.1, moment@^2.22.1:
- version "2.24.0"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
- integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
-
-moment@^2.29.3:
- version "2.29.3"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3"
- integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==
+"moment@>= 2.6.0", moment@^2.10, moment@^2.19.1, moment@^2.22.1, moment@^2.29.4:
+ version "2.29.4"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
+ integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
moo@^0.4.3:
version "0.4.3"
@@ -10608,6 +10806,11 @@ object-inspect@^1.12.0, object-inspect@^1.9.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
+object-inspect@^1.12.2:
+ version "1.12.3"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
+ integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
+
object-inspect@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
@@ -10658,43 +10861,33 @@ object.assign@^4.1.2:
has-symbols "^1.0.1"
object-keys "^1.1.1"
-object.entries@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
- integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==
- dependencies:
- define-properties "^1.1.3"
- es-abstract "^1.12.0"
- function-bind "^1.1.1"
- has "^1.0.3"
-
-object.entries@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b"
- integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==
+object.assign@^4.1.4:
+ version "4.1.4"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
+ integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==
dependencies:
- define-properties "^1.1.3"
- es-abstract "^1.17.0-next.1"
- function-bind "^1.1.1"
- has "^1.0.3"
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ has-symbols "^1.0.3"
+ object-keys "^1.1.1"
-object.entries@^1.1.2, object.entries@^1.1.5:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861"
- integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==
+object.entries@^1.1.0, object.entries@^1.1.1, object.entries@^1.1.2, object.entries@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23"
+ integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
-object.fromentries@^2.0.0, object.fromentries@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251"
- integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==
+object.fromentries@^2.0.0, object.fromentries@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73"
+ integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
object.getownpropertydescriptors@^2.0.3:
version "2.0.3"
@@ -10714,13 +10907,13 @@ object.getownpropertydescriptors@^2.1.0:
define-properties "^1.1.4"
es-abstract "^1.20.1"
-object.hasown@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.1.tgz#ad1eecc60d03f49460600430d97f23882cf592a3"
- integrity sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==
+object.hasown@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92"
+ integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==
dependencies:
define-properties "^1.1.4"
- es-abstract "^1.19.5"
+ es-abstract "^1.20.4"
object.pick@^1.3.0:
version "1.3.0"
@@ -10729,34 +10922,14 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
-object.values@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
- integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
- dependencies:
- define-properties "^1.1.3"
- es-abstract "^1.12.0"
- function-bind "^1.1.1"
- has "^1.0.3"
-
-object.values@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
- integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
- dependencies:
- define-properties "^1.1.3"
- es-abstract "^1.17.0-next.1"
- function-bind "^1.1.1"
- has "^1.0.3"
-
-object.values@^1.1.5:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac"
- integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==
+object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.5, object.values@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d"
+ integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
obuf@^1.0.0, obuf@^1.1.2:
version "1.1.2"
@@ -10856,7 +11029,7 @@ os-homedir@^1.0.0:
os-tmpdir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
- integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+ integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
osenv@^0.1.4:
version "0.1.5"
@@ -11067,7 +11240,7 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
-"parse5@5 - 6", parse5@6.0.1:
+parse5@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
@@ -11350,16 +11523,16 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
-pofile@1.0.x:
- version "1.0.11"
- resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954"
- integrity sha512-Vy9eH1dRD9wHjYt/QqXcTz+RnX/zg53xK+KljFSX30PvdDMb2z+c6uDUeblUGqqJgz3QFsdlA0IJvHziPmWtQg==
-
popper.js@^1.14.4:
version "1.15.0"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
+popper.js@^1.16.0:
+ version "1.16.1"
+ resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
+ integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
+
portfinder@^1.0.26:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
@@ -12183,16 +12356,7 @@ prop-types-extra@^1.1.0:
react-is "^16.3.2"
warning "^4.0.0"
-prop-types@^15, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2:
- version "15.7.2"
- resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
- integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
- dependencies:
- loose-envify "^1.4.0"
- object-assign "^4.1.1"
- react-is "^16.8.1"
-
-prop-types@^15.7.0, prop-types@^15.8.1:
+prop-types@^15, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -12569,6 +12733,16 @@ react-dom@^17.0.2:
object-assign "^4.1.1"
scheduler "^0.20.2"
+react-dropzone@9.0.0:
+ version "9.0.0"
+ resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-9.0.0.tgz#4f5223cdcb4d3bd8a66e3298c4041eb0c75c4634"
+ integrity sha512-wZ2o9B2qkdE3RumWhfyZT9swgJYJPeU5qHEcMU8weYpmLex1eeWX0CC32/Y0VutB+BBi2D+iePV/YZIiB4kZGw==
+ dependencies:
+ attr-accept "^1.1.3"
+ file-selector "^0.1.8"
+ prop-types "^15.6.2"
+ prop-types-extra "^1.1.0"
+
react-ellipsis-with-tooltip@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/react-ellipsis-with-tooltip/-/react-ellipsis-with-tooltip-1.0.8.tgz#fce2d0c9c4820e85ad7fc8c1a77e9a0b86f429fb"
@@ -12612,7 +12786,7 @@ react-is@^16.13.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
-react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
+react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
@@ -13099,7 +13273,7 @@ regex-parser@^2.2.11:
resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58"
integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==
-regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3:
+regexp.prototype.flags@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==
@@ -13280,21 +13454,7 @@ resolve@1.18.1:
is-core-module "^2.0.0"
path-parse "^1.0.6"
-resolve@^1.10.0:
- version "1.11.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232"
- integrity sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==
- dependencies:
- path-parse "^1.0.6"
-
-resolve@^1.12.0:
- version "1.14.2"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2"
- integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ==
- dependencies:
- path-parse "^1.0.6"
-
-resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3.2:
+resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3.2:
version "1.22.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
@@ -13303,13 +13463,14 @@ resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.2
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
-resolve@^2.0.0-next.3:
- version "2.0.0-next.3"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46"
- integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==
+resolve@^2.0.0-next.4:
+ version "2.0.0-next.4"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660"
+ integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==
dependencies:
- is-core-module "^2.2.0"
- path-parse "^1.0.6"
+ is-core-module "^2.9.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
responselike@^1.0.2:
version "1.0.2"
@@ -13464,6 +13625,15 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+safe-regex-test@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295"
+ integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.3"
+ is-regex "^1.1.4"
+
safe-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
@@ -13592,10 +13762,10 @@ semver-utils@^1.1.4:
resolved "https://registry.yarnpkg.com/semver-utils/-/semver-utils-1.1.4.tgz#cf0405e669a57488913909fc1c3f29bf2a4871e2"
integrity sha512-EjnoLE5OGmDAVV/8YDoN5KiajNadjzIp9BAHOhYeQHt7j0UWxjmgsx4YD48wp4Ue1Qogq38F1GNUJNqF1kKKxA==
-"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0:
- version "5.7.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
- integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+ integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
semver@7.0.0:
version "7.0.0"
@@ -13607,11 +13777,6 @@ semver@7.3.2:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
-semver@^5.6.0:
- version "5.7.1"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
- integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
-
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@@ -14006,7 +14171,7 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc
source-map@^0.5.0, source-map@^0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
- integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+ integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
source-map@^0.7.3:
version "0.7.4"
@@ -14300,18 +14465,18 @@ string-width@^4.1.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
-string.prototype.matchall@^4.0.7:
- version "4.0.7"
- resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d"
- integrity sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==
+string.prototype.matchall@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3"
+ integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
- get-intrinsic "^1.1.1"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+ get-intrinsic "^1.1.3"
has-symbols "^1.0.3"
internal-slot "^1.0.3"
- regexp.prototype.flags "^1.4.1"
+ regexp.prototype.flags "^1.4.3"
side-channel "^1.0.4"
string.prototype.padend@^3.0.0:
@@ -14341,6 +14506,15 @@ string.prototype.trimend@^1.0.5:
define-properties "^1.1.4"
es-abstract "^1.19.5"
+string.prototype.trimend@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533"
+ integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
string.prototype.trimleft@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74"
@@ -14366,6 +14540,15 @@ string.prototype.trimstart@^1.0.5:
define-properties "^1.1.4"
es-abstract "^1.19.5"
+string.prototype.trimstart@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4"
+ integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.4"
+ es-abstract "^1.20.4"
+
string_decoder@^1.0.0, string_decoder@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
@@ -14595,6 +14778,11 @@ symbol-tree@^3.2.4:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+tabbable@^5.3.2:
+ version "5.3.3"
+ resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf"
+ integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==
+
table-resolver@^3.2.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/table-resolver/-/table-resolver-3.3.0.tgz#22e575d3c6aed15404ab71a0f2046c4ff55e556e"
@@ -14759,7 +14947,7 @@ through2@^4.0.0:
through@2, "through@>=2.2.7 <3":
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
- integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+ integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
thunky@^1.0.2:
version "1.0.3"
@@ -14793,6 +14981,13 @@ tiny-warning@^1.0.3:
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+tippy.js@5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-5.1.2.tgz#5ac91233c59ab482ef5988cffe6e08bd26528e66"
+ integrity sha512-Qtrv2wqbRbaKMUb6bWWBQWPayvcDKNrGlvihxtsyowhT7RLGEh1STWuy6EMXC6QLkfKPB2MLnf8W2mzql9VDAw==
+ dependencies:
+ popper.js "^1.16.0"
+
tmpl@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
@@ -14921,7 +15116,7 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
-tslib@^2.0.3, tslib@^2.1.0:
+tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
@@ -15002,6 +15197,15 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
+typed-array-length@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"
+ integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==
+ dependencies:
+ call-bind "^1.0.2"
+ for-each "^0.3.3"
+ is-typed-array "^1.1.9"
+
typed-styles@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
@@ -15019,11 +15223,6 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-"typescript@2 - 4":
- version "4.7.3"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d"
- integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==
-
ua-parser-js@^0.7.18:
version "0.7.19"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
@@ -15651,6 +15850,18 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+which-typed-array@^1.1.9:
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6"
+ integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==
+ dependencies:
+ available-typed-arrays "^1.0.5"
+ call-bind "^1.0.2"
+ for-each "^0.3.3"
+ gopd "^1.0.1"
+ has-tostringtag "^1.0.0"
+ is-typed-array "^1.1.10"
+
which@^1.2.9, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"