diff --git a/webpack/components/ConfirmModal.js b/webpack/components/ConfirmModal.js deleted file mode 100644 index 8a35927a6..000000000 --- a/webpack/components/ConfirmModal.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Modal, Button, ModalVariant, Spinner } from '@patternfly/react-core'; - -import { translate as __ } from 'foremanReact/common/I18n'; - -import './ConfirmModal.scss'; - -const ConfirmModal = props => { - const [callMutation, { loading }] = props.prepareMutation(); - - const actions = [ - , - , - ]; - - if (loading) { - actions.push(); - } - - return ( - - {props.text} - - ); -}; - -ConfirmModal.propTypes = { - prepareMutation: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, - record: PropTypes.object, - onClose: PropTypes.func.isRequired, - title: PropTypes.string.isRequired, - isOpen: PropTypes.bool.isRequired, - text: PropTypes.string.isRequired, -}; - -ConfirmModal.defaultProps = { - record: null, -}; - -export default ConfirmModal; diff --git a/webpack/components/ConfirmModal.scss b/webpack/components/ConfirmModal.scss deleted file mode 100644 index 0a5da91f5..000000000 --- a/webpack/components/ConfirmModal.scss +++ /dev/null @@ -1,3 +0,0 @@ -.pf-v5-c-backdrop { - z-index: 1040; -} diff --git a/webpack/components/IndexLayout.js b/webpack/components/IndexLayout.js deleted file mode 100644 index b0b63bd76..000000000 --- a/webpack/components/IndexLayout.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Helmet } from 'react-helmet'; -import ToastsList from 'foremanReact/components/ToastsList'; -import { - Grid, - GridItem, - TextContent, - Text, - TextVariants, -} from '@patternfly/react-core'; - -import './IndexLayout.scss'; - -const IndexLayout = ({ pageTitle, children, contentWidthSpan }) => ( - - - {pageTitle} - - - - - - - {pageTitle} - - - - {children} - - -); - -IndexLayout.propTypes = { - pageTitle: PropTypes.string.isRequired, - children: PropTypes.oneOfType([PropTypes.node, PropTypes.object]).isRequired, - contentWidthSpan: PropTypes.number, -}; - -IndexLayout.defaultProps = { - contentWidthSpan: 12, -}; - -export default IndexLayout; diff --git a/webpack/components/IndexTable/IndexTableHelper.js b/webpack/components/IndexTable/IndexTableHelper.js deleted file mode 100644 index 50102d621..000000000 --- a/webpack/components/IndexTable/IndexTableHelper.js +++ /dev/null @@ -1,6 +0,0 @@ -import { addSearch } from '../../helpers/pageParamsHelper'; - -export const refreshPage = (history, params = {}) => { - const url = addSearch(history.location.pathname, params); - history.push(url); -}; diff --git a/webpack/components/IndexTable/index.js b/webpack/components/IndexTable/index.js deleted file mode 100644 index e39f1ad63..000000000 --- a/webpack/components/IndexTable/index.js +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { - Table, - TableHeader, - TableBody, -} from '@patternfly/react-table/deprecated'; -import { Flex, FlexItem } from '@patternfly/react-core'; -import Pagination from 'foremanReact/components/Pagination'; -import { refreshPage } from './IndexTableHelper'; - -const IndexTable = ({ - history, - pagination, - totalCount, - toolbarBtns, - ariaTableLabel, - ouiaTableId, - columns, - ...rest -}) => { - const handlePerPageSelected = perPage => { - refreshPage(history, { page: 1, perPage }); - }; - - const handlePageSelected = page => { - refreshPage(history, { ...pagination, page }); - }; - - return ( - - - {toolbarBtns} - - - - - - - -
-
- ); -}; - -IndexTable.propTypes = { - history: PropTypes.object.isRequired, - pagination: PropTypes.object.isRequired, - toolbarBtns: PropTypes.node, - totalCount: PropTypes.number.isRequired, - ariaTableLabel: PropTypes.string.isRequired, - ouiaTableId: PropTypes.string.isRequired, - columns: PropTypes.array.isRequired, -}; - -IndexTable.defaultProps = { - toolbarBtns: null, -}; - -export default IndexTable; diff --git a/webpack/components/LineChart/LineChartHelpers.js b/webpack/components/LineChart/LineChartHelpers.js index e72fb6460..5f7d2366e 100644 --- a/webpack/components/LineChart/LineChartHelpers.js +++ b/webpack/components/LineChart/LineChartHelpers.js @@ -1,6 +1,16 @@ /** Backend sends [label, values, color]; legacy Foreman charts used spread columns. */ import { chart_color_black_500 as chartColorBlack500 } from '@patternfly/react-tokens'; +const TIMESTAMP_THRESHOLD = 1e12; +const MS_PER_SECOND = 1000; +const DEFAULT_TICK_MID = 0.5; +const MIN_TICK_STEP = 0.1; +const EXPONENTIAL_THRESHOLD = 1e21; +const CLAMP_SCALE_FACTOR = 0.9; +const HOURS_PER_HALF_DAY = 12; +const MINUTES_PER_HOUR = 60; +const SECONDS_PER_MINUTE = 60; + const getColumnValues = col => { if (Array.isArray(col[1])) return col[1]; @@ -32,7 +42,7 @@ const getSeriesColor = col => { const toMs = val => { const n = Number(val); - return n >= 1e12 ? n : n * 1000; + return n >= TIMESTAMP_THRESHOLD ? n : n * MS_PER_SECOND; }; /** Process raw backend data into chart series for PatternFly line charts. */ @@ -101,9 +111,9 @@ export const getYTickValues = (chartData, hiddenSeries = new Set()) => { }); }); - if (maxY <= 0) return [0, 0.5, 1.0]; + if (maxY <= 0) return [0, DEFAULT_TICK_MID, 1.0]; - const step = Math.max(0.1, Math.ceil((maxY / 4) * 10) / 10); + const step = Math.max(MIN_TICK_STEP, Math.ceil((maxY / 4) * 10) / 10); return [0, 1, 2, 3, 4, 5].map(i => Math.round(i * step * 10) / 10); }; @@ -111,7 +121,7 @@ export const getYTickValues = (chartData, hiddenSeries = new Set()) => { export const formatTooltipValue = value => { const num = Number(value); if (!Number.isFinite(num)) return ''; - if (Math.abs(num) >= 1e21) { + if (Math.abs(num) >= EXPONENTIAL_THRESHOLD) { return num.toExponential(1); } return String(Math.round(num)); @@ -128,14 +138,14 @@ export const clampChartPadding = (padding, width, height) => { const horizontalTotal = left + right; if (horizontalTotal >= width) { - const scale = (width * 0.9) / horizontalTotal; + const scale = (width * CLAMP_SCALE_FACTOR) / horizontalTotal; left *= scale; right *= scale; } const verticalTotal = top + bottom; if (verticalTotal >= height) { - const scale = (height * 0.9) / verticalTotal; + const scale = (height * CLAMP_SCALE_FACTOR) / verticalTotal; top *= scale; bottom *= scale; } @@ -154,7 +164,11 @@ export const getTimeseriesXDomain = chartData => { const max = Math.max(...times); if (min === max) { - const offset = 12 * 60 * 60 * 1000; + const offset = + HOURS_PER_HALF_DAY * + MINUTES_PER_HOUR * + SECONDS_PER_MINUTE * + MS_PER_SECOND; return [new Date(min - offset), new Date(max + offset)]; } diff --git a/webpack/components/LinkButton.js b/webpack/components/LinkButton.js deleted file mode 100644 index 8978e52f8..000000000 --- a/webpack/components/LinkButton.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import { Button } from '@patternfly/react-core'; -import PropTypes from 'prop-types'; - -const LinkButton = ({ - path, - btnVariant, - btnText, - isDisabled, - btnAriaLabel, - ouiaId, -}) => ( - - - -); - -LinkButton.propTypes = { - path: PropTypes.string.isRequired, - btnText: PropTypes.string.isRequired, - btnVariant: PropTypes.string, - isDisabled: PropTypes.bool, - btnAriaLabel: PropTypes.string, - ouiaId: PropTypes.string, -}; - -LinkButton.defaultProps = { - btnVariant: 'primary', - isDisabled: false, - btnAriaLabel: null, - ouiaId: 'oscap-link-button', -}; - -export default LinkButton; diff --git a/webpack/components/OpenscapRemediationWizard/constants.js b/webpack/components/OpenscapRemediationWizard/constants.js index b8f745bde..7d095bd99 100644 --- a/webpack/components/OpenscapRemediationWizard/constants.js +++ b/webpack/components/OpenscapRemediationWizard/constants.js @@ -15,6 +15,9 @@ export const JOB_INVOCATION_API_REQUEST_KEY = 'OPENSCAP_REX_JOB_INVOCATIONS'; export const SNIPPET_SH = 'urn:xccdf:fix:script:sh'; export const SNIPPET_ANSIBLE = 'urn:xccdf:fix:script:ansible'; +export const TOOLTIP_COPIED_EXIT_DELAY_MS = 1500; +export const TOOLTIP_DEFAULT_EXIT_DELAY_MS = 600; + export const WIZARD_TITLES = { snippetSelect: __('Select snippet'), reviewHosts: __('Review hosts'), diff --git a/webpack/components/OpenscapRemediationWizard/steps/Finish.js b/webpack/components/OpenscapRemediationWizard/steps/Finish.js index 30451c6a4..2800c020d 100644 --- a/webpack/components/OpenscapRemediationWizard/steps/Finish.js +++ b/webpack/components/OpenscapRemediationWizard/steps/Finish.js @@ -6,7 +6,7 @@ import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons'; import { translate as __ } from 'foremanReact/common/I18n'; import { foremanUrl } from 'foremanReact/common/helpers'; -import { STATUS } from 'foremanReact/constants'; +import { STATUS, HTTP_STATUS_CODES } from 'foremanReact/constants'; import { useAPI } from 'foremanReact/common/hooks/API/APIHooks'; import Loading from 'foremanReact/components/Loading'; import PermissionDenied from 'foremanReact/components/PermissionDenied'; @@ -91,7 +91,7 @@ const Finish = ({ onClose }) => { ); const errorComponent = - statusCode === 403 ? ( + statusCode === HTTP_STATUS_CODES.FORBIDDEN ? ( { textId="code-content" aria-label="Copy to clipboard" onClick={e => onCopyClick(e, snippetText)} - exitDelay={copied ? 1500 : 600} + exitDelay={ + copied + ? TOOLTIP_COPIED_EXIT_DELAY_MS + : TOOLTIP_DEFAULT_EXIT_DELAY_MS + } maxWidth="110px" variant="plain" onTooltipHidden={() => setCopied(false)} diff --git a/webpack/components/OpenscapRemediationWizard/steps/SnippetSelect.js b/webpack/components/OpenscapRemediationWizard/steps/SnippetSelect.js index 7203183bb..4b075df8f 100644 --- a/webpack/components/OpenscapRemediationWizard/steps/SnippetSelect.js +++ b/webpack/components/OpenscapRemediationWizard/steps/SnippetSelect.js @@ -18,6 +18,8 @@ import WizardHeader from '../WizardHeader'; import EmptyState from '../../EmptyState'; import { errorMsg, supportedRemediationSnippets } from '../helpers'; +const URN_TAIL_SEGMENTS = -2; + const SnippetSelect = () => { const { fixes, @@ -44,7 +46,7 @@ const SnippetSelect = () => { if (mapped) return mapped; return join( - map(slice(split(system, ':'), -2), n => capitalize(n)), + map(slice(split(system, ':'), URN_TAIL_SEGMENTS), n => capitalize(n)), ' ' ); }; diff --git a/webpack/components/withDeleteModal.js b/webpack/components/withDeleteModal.js deleted file mode 100644 index 6dd89da30..000000000 --- a/webpack/components/withDeleteModal.js +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { translate as __, sprintf } from 'foremanReact/common/I18n'; -import ConfirmModal from './ConfirmModal'; - -const withDelete = Component => { - const Subcomponent = ({ - confirmDeleteTitle, - submitDelete, - prepareMutation, - ...rest - }) => { - const [toDelete, setToDelete] = useState(null); - - const toggleModal = (item = null) => { - setToDelete(item); - }; - - return ( - - - prepareMutation(toggleModal)} - record={toDelete} - /> - - ); - }; - - Subcomponent.propTypes = { - confirmDeleteTitle: PropTypes.string.isRequired, - submitDelete: PropTypes.func.isRequired, - prepareMutation: PropTypes.func.isRequired, - }; - - return Subcomponent; -}; - -export default withDelete; diff --git a/webpack/components/withLoading.js b/webpack/components/withLoading.js deleted file mode 100644 index ed9d54936..000000000 --- a/webpack/components/withLoading.js +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { translate as __ } from 'foremanReact/common/I18n'; -import Loading from 'foremanReact/components/Loading'; -import { - permissionCheck, - permissionDeniedMsg, -} from '../helpers/permissionsHelper'; - -import EmptyState from './EmptyState'; - -const errorStateTitle = __('Error!'); - -const pluckData = (data, path) => { - const split = path.split('.'); - return split.reduce((memo, item) => { - if (item) { - return memo[item]; - } - throw new Error('Unexpected empty segment in response data path'); - }, data); -}; - -const withLoading = Component => { - const Subcomponent = ({ - fetchFn, - resultPath, - renameData, - emptyStateTitle, - emptyStateBody, - permissions, - primaryButton, - shouldRefetch, - ...rest - }) => { - const { loading, error, data, refetch } = fetchFn(rest); - - useEffect(() => { - if (shouldRefetch) { - refetch(); - } - }, [shouldRefetch, refetch]); - - if (loading) { - return ; - } - - if (error) { - return ( - - ); - } - - const check = permissionCheck(data.currentUser, permissions); - - if (!check.allowed) { - return ( - item.name))} - /> - ); - } - - const result = pluckData(data, resultPath); - - if ((Array.isArray(result) && result.length === 0) || !result) { - return ( - - ); - } - - return ; - }; - - Subcomponent.propTypes = { - fetchFn: PropTypes.func.isRequired, - resultPath: PropTypes.string.isRequired, - renameData: PropTypes.func, - emptyStateTitle: PropTypes.string.isRequired, - emptyStateBody: PropTypes.string, - permissions: PropTypes.array, - primaryButton: PropTypes.node, - shouldRefetch: PropTypes.bool, - }; - - Subcomponent.defaultProps = { - renameData: data => data, - permissions: [], - primaryButton: null, - shouldRefetch: false, - emptyStateBody: '', - }; - - return Subcomponent; -}; - -export default withLoading; diff --git a/webpack/global_index.js b/webpack/global_index.js index d62e51fa3..30d3192e1 100644 --- a/webpack/global_index.js +++ b/webpack/global_index.js @@ -2,9 +2,11 @@ import React from 'react'; import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill'; import HostKebabItems from './components/HostExtentions/HostKebabItems'; +const OPENSCAP_KEBAB_WEIGHT = 400; + addGlobalFill( 'host-details-kebab', `openscap-kebab-items`, , - 400 + OPENSCAP_KEBAB_WEIGHT ); diff --git a/webpack/helpers/commonHelper.js b/webpack/helpers/commonHelper.js deleted file mode 100644 index b4c041352..000000000 --- a/webpack/helpers/commonHelper.js +++ /dev/null @@ -1 +0,0 @@ -export const last = array => array[array.length - 1]; diff --git a/webpack/helpers/globalIdHelper.js b/webpack/helpers/globalIdHelper.js deleted file mode 100644 index 8eb3d5da1..000000000 --- a/webpack/helpers/globalIdHelper.js +++ /dev/null @@ -1,15 +0,0 @@ -import { last } from './commonHelper'; - -const idSeparator = '-'; -const versionSeparator = ':'; -const defaultVersion = '01'; - -export const decodeModelId = model => decodeId(model.id); - -export const decodeId = globalId => { - const split = atob(globalId).split(idSeparator); - return parseInt(last(split), 10); -}; - -export const encodeId = (typename, id) => - btoa([defaultVersion, versionSeparator, typename, idSeparator, id].join('')); diff --git a/webpack/helpers/mutationHelper.js b/webpack/helpers/mutationHelper.js deleted file mode 100644 index 0ff14e84d..000000000 --- a/webpack/helpers/mutationHelper.js +++ /dev/null @@ -1,68 +0,0 @@ -import { useMutation } from '@apollo/client'; -import { translate as __, sprintf } from 'foremanReact/common/I18n'; - -import { useCurrentPagination, pageToVars } from './pageParamsHelper'; - -const formatError = (error, name) => - sprintf(__('There was a following error when deleting %(name)s: %(error)s'), { - name, - error, - }); - -const joinErrors = errors => errors.map(err => err.message).join(', '); - -const onError = (showToast, resourceName) => error => { - showToast({ type: 'error', message: formatError(error, resourceName) }); -}; - -const onCompleted = ( - toggleModal, - showToast, - mutationName, - successMsg, - resourceName -) => data => { - toggleModal(); - const { errors } = data[mutationName]; - if (Array.isArray(errors) && errors.length > 0) { - showToast({ - type: 'error', - message: formatError(joinErrors(errors), resourceName), - }); - } else { - showToast({ - type: 'success', - message: successMsg, - }); - } -}; - -export const prepareMutation = ( - history, - showToast, - refetchQuery, - mutationName, - successMsg, - mutation, - resourceName -) => toggleModal => { - const pagination = pageToVars(useCurrentPagination(history)); - - const options = { - refetchQueries: [{ query: refetchQuery, variables: pagination }], - onCompleted: onCompleted( - toggleModal, - showToast, - mutationName, - successMsg, - resourceName - ), - onError: onError(showToast, resourceName), - }; - - return useMutation(mutation, options); -}; - -export const submitDelete = (mutation, id) => { - mutation({ variables: { id } }); -}; diff --git a/webpack/helpers/pageParamsHelper.js b/webpack/helpers/pageParamsHelper.js deleted file mode 100644 index cbd247f7c..000000000 --- a/webpack/helpers/pageParamsHelper.js +++ /dev/null @@ -1,31 +0,0 @@ -import URI from 'urijs'; -import { useForemanSettings } from 'foremanReact/Root/Context/ForemanContext'; - -const parsePageParams = history => URI.parseQuery(history.location.search); - -export const addSearch = (basePath, params) => { - let stringyfied = ''; - if (Object.keys(params).length > 0) { - stringyfied = `?${URI.buildQuery(params)}`; - } - - return `${basePath}${stringyfied}`; -}; - -export const useCurrentPagination = history => { - const pageParams = parsePageParams(history); - const uiSettings = useForemanSettings(); - - return { - page: parseInt(pageParams.page, 10) || 1, - perPage: parseInt(pageParams.perPage, 10) || uiSettings.perPage, - }; -}; - -export const pageToVars = pagination => ({ - first: pagination.page * pagination.perPage, - last: pagination.perPage, -}); - -export const useParamsToVars = history => - pageToVars(useCurrentPagination(history)); diff --git a/webpack/helpers/permissionsHelper.js b/webpack/helpers/permissionsHelper.js deleted file mode 100644 index 7763a2ef3..000000000 --- a/webpack/helpers/permissionsHelper.js +++ /dev/null @@ -1,42 +0,0 @@ -import { translate as __, sprintf } from 'foremanReact/common/I18n'; - -export const permissionCheck = (user, permissionsRequired) => { - if (permissionsRequired.length === 0) { - return { allowed: true }; - } - - if (!user) { - throw new Error( - 'No user data when loading the page - cannot determine if current user is allowed to view the page.' - ); - } - - if (user.admin) { - return { allowed: true }; - } - - const permList = permissionsRequired.reduce((memo, item) => { - const found = user.permissions.nodes.find( - permission => permission.name === item - ); - memo.push({ name: item, present: !!found }); - return memo; - }, []); - - if (permList.reduce((memo, item) => memo && item.present, true)) { - return { allowed: true, permissions: permList }; - } - - return { allowed: false, permissions: permList }; -}; - -export const permissionDeniedMsg = permissions => { - let msg = __('You are not authorized to view the page. '); - if (permissions?.length > 0) { - msg += sprintf( - __('Request the following permissions from administrator: %s.'), - permissions.join(', ') - ); - } - return msg; -}; diff --git a/webpack/helpers/tableHelper.js b/webpack/helpers/tableHelper.js deleted file mode 100644 index 5bc03db4a..000000000 --- a/webpack/helpers/tableHelper.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import { TableText } from '@patternfly/react-table'; - -export const linkCell = (path, text) => ( - - {text} - -); diff --git a/webpack/helpers/toastHelper.js b/webpack/helpers/toastHelper.js deleted file mode 100644 index b4c92cbb1..000000000 --- a/webpack/helpers/toastHelper.js +++ /dev/null @@ -1,3 +0,0 @@ -import { addToast } from 'foremanReact/components/ToastsList'; - -export const showToast = dispatch => toast => dispatch(addToast(toast)); diff --git a/webpack/testHelper.js b/webpack/testHelper.js deleted file mode 100644 index 225604139..000000000 --- a/webpack/testHelper.js +++ /dev/null @@ -1,127 +0,0 @@ -import React, { useState } from 'react'; -import { Provider } from 'react-redux'; -import store from 'foremanReact/redux'; -import { MockedProvider } from '@apollo/react-testing'; -import { MemoryRouter } from 'react-router-dom'; -import { getForemanContext } from 'foremanReact/Root/Context/ForemanContext'; -import { waitFor } from '@testing-library/react'; - -export const withRedux = Component => props => ( - - - -); - -export const withRouter = Component => props => ( - - - -); - -export const withMockedProvider = Component => props => { - // eslint-disable-next-line react/prop-types - const { mocks, ...rest } = props; - - const [context, setContext] = useState({ - metadata: { - UISettings: { - perPage: 20, - }, - }, - }); - - const contextData = { context, setContext }; - const ForemanContext = getForemanContext(contextData); - return ( - - - - - - ); -}; - -// use to resolve async mock requests for apollo MockedProvider -export const tick = () => new Promise(resolve => setTimeout(resolve, 0)); - -export const wait = async (tickCount = 1) => { - for (let i = 1; i < tickCount; i++) { - // eslint-disable-next-line no-await-in-loop - await waitFor(tick); - } - return waitFor(tick); -}; - -export const historyMock = { - location: { - search: '', - }, -}; - -export const admin = { - __typename: 'User', - id: 'MDE6VXNlci00', - login: 'admin', - admin: true, - permissions: { - nodes: [], - }, -}; - -export const userFactory = (login, permissions = []) => ({ - __typename: 'User', - id: 'MDE6VXNlci01', - login, - admin: false, - permissions: { - nodes: permissions, - }, -}); - -export const intruder = userFactory('intruder', [ - { - __typename: 'Permission', - id: 'MDE6UGVybWlzc2lvbi0x', - name: 'view_architectures', - }, -]); - -export const viewer = userFactory('viewer', [ - { - __typename: 'Permission', - id: 'MDE6UGVybWlzc2lvbi0yOTY=', - name: 'view_oval_contents', - }, - { - __typename: 'Permission', - id: 'MDE6UGVybWlzc2lvbi0yNzU=', - name: 'view_oval_policies', - }, -]); - -export const mockFactory = (resultName, query) => ( - variables, - modelResults, - { errors = [], currentUser = null } = {} -) => { - const mock = { - request: { - query, - variables, - }, - result: { - data: { - [resultName]: modelResults, - }, - }, - }; - - if (errors.length !== 0) { - mock.result.errors = errors; - } - - if (currentUser) { - mock.result.data.currentUser = currentUser; - } - return [mock]; -};