Bump react scripts to 5.0

Bump react scripts to 5.0

See: https://github.com/ansible/awx/issues/11543

Bump eslint

Bump eslint and related plugins

Add @babe/core

Add @babe/core remove babel/core.

Rename .eslintrc to .eslintrc.json

Rename .eslintrc to .eslintrc.json

Add extra plugin

Move babe-plugin-macro as dev dependencies

Move babe-plugin-macro as dev dependencies

Add preset-react

Add preset-react

Fixing lint errors

Fixing lint errors

Run eslint --fix

Run eslint --fix

Turn no-restricted-exports off

Turn no-restricted-exports off

Revert "Run eslint --fix"

This reverts commit e760885b6c199f2ca18091088cb79bfa77c1d3ed.

Run --fix

Run --fix

Fix lint errors

Also bump specificity of Select CSS border component to avoid bug of
missing borders.

Also update API tests related to lincenses.
This commit is contained in:
nixocio
2022-01-19 18:02:13 -05:00
parent 54cbf13219
commit 9703fb06fc
97 changed files with 15536 additions and 25938 deletions

0
awx/ui/.babel.rc Normal file
View File

View File

@@ -7,4 +7,5 @@ build
node_modules
dist
images
instrumented
instrumented
*test*.js

View File

@@ -1,14 +1,19 @@
{
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"ignorePatterns": ["./node_modules/"],
"parserOptions": {
"requireConfigFile": false,
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"modules": true
}
},
"babelOptions": {
"presets": ["@babel/preset-react"]
}
},
"plugins": ["react-hooks", "jsx-a11y", "i18next"],
"plugins": ["react-hooks", "jsx-a11y", "i18next", "@babel"],
"extends": [
"airbnb",
"prettier",
@@ -17,7 +22,7 @@
],
"settings": {
"react": {
"version": "16.5.2"
"version": "detect"
},
"import/resolver": {
"node": {
@@ -141,6 +146,9 @@
"jsx-a11y/label-has-associated-control": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/jsx-filename-extension": "off"
"react/jsx-filename-extension": "off",
"no-restricted-exports": "off",
"react/function-component-definition": "off",
"prefer-regex-literals": "off"
}
}

View File

@@ -1,4 +1,4 @@
FROM node:14
FROM node:14.18.3
ARG NPMRC_FILE=.npmrc
ENV NPMRC_FILE=${NPMRC_FILE}
ARG TARGET='https://awx:8043'
@@ -6,7 +6,7 @@ ENV TARGET=${TARGET}
ENV CI=true
WORKDIR /ui
ADD .eslintignore .eslintignore
ADD .eslintrc .eslintrc
ADD .eslintrc.json .eslintrc.json
ADD .linguirc .linguirc
ADD jsconfig.json jsconfig.json
ADD public public

View File

@@ -1,7 +1,7 @@
# AWX-UI
## Requirements
- node 14.x LTS, npm 7.x, make, git
- node 14.18.3, npm 7.24.2, make, git
## Development
The API development server will need to be running. See [CONTRIBUTING.md](../../CONTRIBUTING.md).

39260
awx/ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,6 @@
"ace-builds": "^1.4.12",
"ansi-to-html": "0.7.2",
"axios": "0.22.0",
"babel-plugin-macros": "^3.0.1",
"codemirror": "^5.47.0",
"d3": "7.1.1",
"dagre": "^0.8.4",
@@ -34,31 +33,36 @@
"styled-components": "5.3.0"
},
"devDependencies": {
"@babel/core": "^7.16.10",
"@babel/eslint-parser": "^7.16.5",
"@babel/eslint-plugin": "^7.16.5",
"@babel/plugin-syntax-jsx": "7.16.7",
"@babel/polyfill": "^7.8.7",
"@babel/preset-react": "7.16.7",
"@cypress/instrument-cra": "^1.4.0",
"@lingui/cli": "^3.7.1",
"@lingui/loader": "^3.8.3",
"@lingui/macro": "^3.7.1",
"@nteract/mockument": "^1.0.4",
"@wojtekmaj/enzyme-adapter-react-17": "0.6.5",
"babel-core": "^7.0.0-bridge.0",
"babel-plugin-macros": "3.1.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"enzyme-to-json": "^3.3.5",
"eslint": "7.30.0",
"eslint-config-airbnb": "18.2.1",
"eslint": "^8.7.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "8.3.0",
"eslint-import-resolver-webpack": "0.11.1",
"eslint-plugin-i18next": "^5.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.11.1",
"eslint-plugin-react-hooks": "4.2.0",
"eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-i18next": "5.1.2",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-react": "7.28.0",
"eslint-plugin-react-hooks": "4.3.0",
"http-proxy-middleware": "^1.0.3",
"jest-websocket-mock": "^2.0.2",
"mock-socket": "^9.0.3",
"prettier": "2.3.2",
"react-scripts": "^4.0.3"
"react-scripts": "5.0.0"
},
"scripts": {
"prelint": "lingui compile",
@@ -66,7 +70,7 @@
"prestart-instrumented": "lingui compile",
"pretest": "lingui compile",
"pretest-watch": "lingui compile",
"start": "ESLINT_NO_DEV_ERRORS=true PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start",
"start": "GENERATE_SOURCEMAP=false ESLINT_NO_DEV_ERRORS=true PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start",
"start-instrumented": "ESLINT_NO_DEV_ERRORS=true DEBUG=instrument-cra PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts -r @cypress/instrument-cra start",
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
"test": "TZ='UTC' react-scripts test --watchAll=false",

View File

@@ -1,3 +1,4 @@
/* eslint-disable default-param-last */
import axios from 'axios';
import { encodeQueryString } from 'util/qs';
import debounce from 'util/debounce';

View File

@@ -1,7 +1,11 @@
.pf-c-select__toggle:before {
border-top: var(--pf-c-select__toggle--before--BorderTopWidth) solid var(--pf-c-select__toggle--before--BorderTopColor);
border-right: var(--pf-c-select__toggle--before--BorderRightWidth) solid var(--pf-c-select__toggle--before--BorderRightColor);
border-bottom: var(--pf-c-select__toggle--before--BorderBottomWidth) solid var(--pf-c-select__toggle--before--BorderBottomColor);
border-left: var(--pf-c-select__toggle--before--BorderLeftWidth) solid var(--pf-c-select__toggle--before--BorderLeftColor);
.pf-c-select .pf-c-select__toggle:before {
border-top: var(--pf-c-select__toggle--before--BorderTopWidth) solid
var(--pf-c-select__toggle--before--BorderTopColor);
border-right: var(--pf-c-select__toggle--before--BorderRightWidth) solid
var(--pf-c-select__toggle--before--BorderRightColor);
border-bottom: var(--pf-c-select__toggle--before--BorderBottomWidth) solid
var(--pf-c-select__toggle--before--BorderBottomColor);
border-left: var(--pf-c-select__toggle--before--BorderLeftWidth) solid
var(--pf-c-select__toggle--before--BorderLeftColor);
}
/* https://github.com/patternfly/patternfly-react/issues/5650 */

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useState, useRef, useEffect } from 'react';
import { t } from '@lingui/macro';
import PropTypes from 'prop-types';

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { t } from '@lingui/macro';

View File

@@ -91,76 +91,74 @@ function AssociateModal({
};
return (
<>
<Modal
ouiaId={ouiaId}
variant="large"
title={title}
aria-label={t`Association modal`}
isOpen={isModalOpen}
onClose={handleClose}
actions={[
<Button
ouiaId="associate-modal-save"
aria-label={t`Save`}
key="select"
variant="primary"
onClick={handleSave}
isDisabled={selected.length === 0}
>
{t`Save`}
</Button>,
<Button
ouiaId="associate-modal-cancel"
aria-label={t`Cancel`}
key="cancel"
variant="link"
onClick={handleClose}
>
{t`Cancel`}
</Button>,
<Modal
ouiaId={ouiaId}
variant="large"
title={title}
aria-label={t`Association modal`}
isOpen={isModalOpen}
onClose={handleClose}
actions={[
<Button
ouiaId="associate-modal-save"
aria-label={t`Save`}
key="select"
variant="primary"
onClick={handleSave}
isDisabled={selected.length === 0}
>
{t`Save`}
</Button>,
<Button
ouiaId="associate-modal-cancel"
aria-label={t`Cancel`}
key="cancel"
variant="link"
onClick={handleClose}
>
{t`Cancel`}
</Button>,
]}
>
<OptionsList
displayKey={displayKey}
contentError={contentError}
columns={columns}
deselectItem={handleSelect}
header={header}
isLoading={isLoading}
multiple
optionCount={itemCount}
options={items}
qsConfig={QS_CONFIG(displayKey)}
readOnly={false}
selectItem={handleSelect}
value={selected}
searchColumns={[
{
name: t`Name`,
key: `${displayKey}__icontains`,
isDefault: true,
},
{
name: t`Created By (Username)`,
key: 'created_by__username__icontains',
},
{
name: t`Modified By (Username)`,
key: 'modified_by__username__icontains',
},
]}
>
<OptionsList
displayKey={displayKey}
contentError={contentError}
columns={columns}
deselectItem={handleSelect}
header={header}
isLoading={isLoading}
multiple
optionCount={itemCount}
options={items}
qsConfig={QS_CONFIG(displayKey)}
readOnly={false}
selectItem={handleSelect}
value={selected}
searchColumns={[
{
name: t`Name`,
key: `${displayKey}__icontains`,
isDefault: true,
},
{
name: t`Created By (Username)`,
key: 'created_by__username__icontains',
},
{
name: t`Modified By (Username)`,
key: 'modified_by__username__icontains',
},
]}
sortColumns={[
{
name: t`Name`,
key: `${displayKey}`,
},
]}
searchableKeys={searchableKeys}
relatedSearchableKeys={relatedSearchableKeys}
/>
</Modal>
</>
sortColumns={[
{
name: t`Name`,
key: `${displayKey}`,
},
]}
searchableKeys={searchableKeys}
relatedSearchableKeys={relatedSearchableKeys}
/>
</Modal>
);
}

View File

@@ -1,4 +1,5 @@
import React, { Fragment } from 'react';
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import { Link, Redirect } from 'react-router-dom';
import { bool, instanceOf } from 'prop-types';
import { t } from '@lingui/macro';

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { useLocation } from 'react-router-dom';
@@ -77,6 +77,14 @@ function DataListToolbar({
setIsKebabOpen(false);
}
}, [isKebabModalOpen]);
const kebabProviderValue = useMemo(
() => ({
isKebabified: true,
onKebabModalChange: setIsKebabModalOpen,
}),
[setIsKebabModalOpen]
);
return (
<Toolbar
id={`${qsConfig.namespace}-list-toolbar`}
@@ -145,25 +153,18 @@ function DataListToolbar({
</ToolbarToggleGroup>
{showExpandCollapse && (
<ToolbarGroup>
<>
<ToolbarItem>
<ExpandCollapse
isCompact={isCompact}
onCompact={onCompact}
onExpand={onExpand}
/>
</ToolbarItem>
</>
<ToolbarItem>
<ExpandCollapse
isCompact={isCompact}
onCompact={onCompact}
onExpand={onExpand}
/>
</ToolbarItem>
</ToolbarGroup>
)}
{isAdvancedSearchShown && additionalControls.length > 0 && (
<ToolbarItem>
<KebabifiedProvider
value={{
isKebabified: true,
onKebabModalChange: setIsKebabModalOpen,
}}
>
<KebabifiedProvider value={kebabProviderValue}>
<Dropdown
toggle={
<KebabToggle

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { TextList, TextListVariants } from '@patternfly/react-core';
import styled from 'styled-components';
const DetailList = ({ children, stacked, compact, ...props }) => (
const DetailList = ({ children, stacked, ...props }) => (
<TextList component={TextListVariants.dl} {...props}>
{children}
</TextList>

View File

@@ -22,10 +22,10 @@ function DisassociateButton({
const [isOpen, setIsOpen] = useState(false);
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
function handleDisassociate() {
const handleDisassociate = () => {
onDisassociate();
setIsOpen(false);
}
};
useEffect(() => {
if (isKebabified) {

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { t } from '@lingui/macro';

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import PropTypes from 'prop-types';
import { useField } from 'formik';

View File

@@ -35,9 +35,9 @@ function CredentialsStep({
name: 'credentials',
validate: (val) =>
credentialsValidator(
defaultCredentials,
allowCredentialsWithPasswords,
val
val,
defaultCredentials
),
});
const [selectedType, setSelectedType] = useState(null);
@@ -102,9 +102,9 @@ function CredentialsStep({
useEffect(() => {
helpers.setError(
credentialsValidator(
defaultCredentials,
allowCredentialsWithPasswords,
field.value
field.value,
defaultCredentials
)
);
/* eslint-disable-next-line react-hooks/exhaustive-deps */

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import styled from 'styled-components';
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
import { Tooltip } from '@patternfly/react-core';

View File

@@ -19,18 +19,16 @@ function StepName({ hasErrors, children, id }) {
return <div id={id}>{children}</div>;
}
return (
<>
<AlertText id={id}>
{children}
<Tooltip
position="right"
content={t`This step contains errors`}
trigger="click mouseenter focus"
>
<ExclamationCircleIcon css="color: var(--pf-global--danger-color--100)" />
</Tooltip>
</AlertText>
</>
<AlertText id={id}>
{children}
<Tooltip
position="right"
content={t`This step contains errors`}
trigger="click mouseenter focus"
>
<ExclamationCircleIcon css="color: var(--pf-global--danger-color--100)" />
</Tooltip>
</AlertText>
);
}

View File

@@ -7,9 +7,9 @@ const credentialPromptsForPassword = (credential) =>
credential?.inputs?.vault_password === 'ASK';
export default function credentialsValidator(
defaultCredentials = [],
allowCredentialsWithPasswords,
selectedCredentials
selectedCredentials,
defaultCredentials = []
) {
if (defaultCredentials.length > 0 && selectedCredentials) {
const missingCredentialTypes = [];

View File

@@ -35,9 +35,9 @@ export default function useCredentialsStep(
validate: () => {
helpers.setError(
credentialsValidator(
resourceDefaultCredentials,
allowCredentialsWithPasswords,
field.value
field.value,
resourceDefaultCredentials
)
);
},

View File

@@ -7,7 +7,7 @@ import useOtherPromptsStep from './steps/useOtherPromptsStep';
import useSurveyStep from './steps/useSurveyStep';
import usePreviewStep from './steps/usePreviewStep';
function showCredentialPasswordsStep(credentials = [], launchConfig) {
function showCredentialPasswordsStep(launchConfig, credentials = []) {
if (
!launchConfig?.ask_credential_on_launch &&
launchConfig?.passwords_needed_to_start
@@ -53,7 +53,7 @@ export default function useLaunchSteps(launchConfig, surveyConfig, resource) {
),
useCredentialPasswordsStep(
launchConfig,
showCredentialPasswordsStep(formikValues.credentials, launchConfig),
showCredentialPasswordsStep(launchConfig, formikValues.credentials),
visited
),
useOtherPromptsStep(launchConfig, resource),

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import PropTypes from 'prop-types';
import { useHistory, useLocation } from 'react-router-dom';

View File

@@ -150,50 +150,48 @@ function ExecutionEnvironmentLookup({
}, [fetchExecutionEnvironments]);
const renderLookup = () => (
<>
<Lookup
id={id}
header={t`Execution Environment`}
value={value}
onBlur={onBlur}
onChange={onChange}
onDebounce={checkExecutionEnvironmentName}
fieldName={fieldName}
validate={validate}
qsConfig={QS_CONFIG}
isLoading={isLoading || isProjectLoading}
isDisabled={isDisabled}
renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList
value={state.selectedItems}
options={executionEnvironments}
optionCount={count}
searchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
]}
sortColumns={[
{
name: t`Name`,
key: 'name',
},
]}
searchableKeys={searchableKeys}
relatedSearchableKeys={relatedSearchableKeys}
multiple={state.multiple}
header={t`Execution Environment`}
name="executionEnvironments"
qsConfig={QS_CONFIG}
readOnly={!canDelete}
selectItem={(item) => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={(item) => dispatch({ type: 'DESELECT_ITEM', item })}
/>
)}
/>
</>
<Lookup
id={id}
header={t`Execution Environment`}
value={value}
onBlur={onBlur}
onChange={onChange}
onDebounce={checkExecutionEnvironmentName}
fieldName={fieldName}
validate={validate}
qsConfig={QS_CONFIG}
isLoading={isLoading || isProjectLoading}
isDisabled={isDisabled}
renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList
value={state.selectedItems}
options={executionEnvironments}
optionCount={count}
searchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
]}
sortColumns={[
{
name: t`Name`,
key: 'name',
},
]}
searchableKeys={searchableKeys}
relatedSearchableKeys={relatedSearchableKeys}
multiple={state.multiple}
header={t`Execution Environment`}
name="executionEnvironments"
qsConfig={QS_CONFIG}
readOnly={!canDelete}
selectItem={(item) => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={(item) => dispatch({ type: 'DESELECT_ITEM', item })}
/>
)}
/>
);
const renderLabel = () => {

View File

@@ -123,71 +123,69 @@ function InventoryLookup({
}, [fetchInventories]);
return isPromptableField ? (
<>
<FieldWithPrompt
fieldId={fieldId}
isRequired={required}
label={t`Inventory`}
promptId={promptId}
promptName={promptName}
isDisabled={!canEdit || isDisabled}
tooltip={t`Select the inventory containing the hosts
<FieldWithPrompt
fieldId={fieldId}
isRequired={required}
label={t`Inventory`}
promptId={promptId}
promptName={promptName}
isDisabled={!canEdit || isDisabled}
tooltip={t`Select the inventory containing the hosts
you want this job to manage.`}
>
<Lookup
id="inventory-lookup"
header={t`Inventory`}
value={value}
onChange={onChange}
onBlur={onBlur}
required={required}
onDebounce={checkInventoryName}
fieldName={fieldName}
validate={validate}
isLoading={isLoading}
isDisabled={!canEdit || isDisabled}
qsConfig={QS_CONFIG}
renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList
value={state.selectedItems}
options={inventories}
optionCount={count}
searchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
{
name: t`Created By (Username)`,
key: 'created_by__username__icontains',
},
{
name: t`Modified By (Username)`,
key: 'modified_by__username__icontains',
},
]}
sortColumns={[
{
name: t`Name`,
key: 'name',
},
]}
searchableKeys={searchableKeys}
relatedSearchableKeys={relatedSearchableKeys}
multiple={state.multiple}
header={t`Inventory`}
name="inventory"
qsConfig={QS_CONFIG}
readOnly={!canDelete}
selectItem={(item) => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={(item) => dispatch({ type: 'DESELECT_ITEM', item })}
/>
)}
/>
<LookupErrorMessage error={error} />
</FieldWithPrompt>
</>
>
<Lookup
id="inventory-lookup"
header={t`Inventory`}
value={value}
onChange={onChange}
onBlur={onBlur}
required={required}
onDebounce={checkInventoryName}
fieldName={fieldName}
validate={validate}
isLoading={isLoading}
isDisabled={!canEdit || isDisabled}
qsConfig={QS_CONFIG}
renderOptionsList={({ state, dispatch, canDelete }) => (
<OptionsList
value={state.selectedItems}
options={inventories}
optionCount={count}
searchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
{
name: t`Created By (Username)`,
key: 'created_by__username__icontains',
},
{
name: t`Modified By (Username)`,
key: 'modified_by__username__icontains',
},
]}
sortColumns={[
{
name: t`Name`,
key: 'name',
},
]}
searchableKeys={searchableKeys}
relatedSearchableKeys={relatedSearchableKeys}
multiple={state.multiple}
header={t`Inventory`}
name="inventory"
qsConfig={QS_CONFIG}
readOnly={!canDelete}
selectItem={(item) => dispatch({ type: 'SELECT_ITEM', item })}
deselectItem={(item) => dispatch({ type: 'DESELECT_ITEM', item })}
/>
)}
/>
<LookupErrorMessage error={error} />
</FieldWithPrompt>
) : (
<>
<Lookup

View File

@@ -1,4 +1,5 @@
import React, { Fragment } from 'react';
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import { func, string } from 'prop-types';
import { Button } from '@patternfly/react-core';

View File

@@ -21,12 +21,12 @@ function RoutedTabs({ tabsArray }) {
return 0;
};
function handleTabSelect(event, eventKey) {
const handleTabSelect = (event, eventKey) => {
const match = tabsArray.find((tab) => tab.id === eventKey);
if (match) {
history.push(match.link);
}
}
};
return (
<Tabs

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { t } from '@lingui/macro';

View File

@@ -164,37 +164,35 @@ function Search({
/>
)) ||
(options && (
<>
<Select
variant={SelectVariant.checkbox}
aria-label={name}
typeAheadAriaLabel={name}
onToggle={setIsFilterDropdownOpen}
onSelect={(event, selection) =>
handleFilterDropdownSelect(key, event, selection)
}
selections={chipsByKey[key].chips.map((chip) => {
const [, ...value] = chip.key.split(':');
return value.join(':');
})}
isOpen={isFilterDropdownOpen}
placeholderText={t`Filter By ${name}`}
ouiaId={`filter-by-${key}`}
isDisabled={isDisabled}
maxHeight={maxSelectHeight}
noResultsFoundText={t`No results found`}
>
{options.map(([optionKey, optionLabel]) => (
<SelectOption
key={optionKey}
value={optionKey}
inputId={`select-option-${optionKey}`}
>
{optionLabel}
</SelectOption>
))}
</Select>
</>
<Select
variant={SelectVariant.checkbox}
aria-label={name}
typeAheadAriaLabel={name}
onToggle={setIsFilterDropdownOpen}
onSelect={(event, selection) =>
handleFilterDropdownSelect(key, event, selection)
}
selections={chipsByKey[key].chips.map((chip) => {
const [, ...value] = chip.key.split(':');
return value.join(':');
})}
isOpen={isFilterDropdownOpen}
placeholderText={t`Filter By ${name}`}
ouiaId={`filter-by-${key}`}
isDisabled={isDisabled}
maxHeight={maxSelectHeight}
noResultsFoundText={t`No results found`}
>
{options.map(([optionKey, optionLabel]) => (
<SelectOption
key={optionKey}
value={optionKey}
inputId={`select-option-${optionKey}`}
>
{optionLabel}
</SelectOption>
))}
</Select>
)) ||
(isBoolean && (
<Select

View File

@@ -37,7 +37,7 @@ function DraggableSelectedList({ selected, onRemove, onRowDrag }) {
return result;
}
function dragItem(item, dest) {
const dragItem = (item, dest) => {
if (!dest || item.index === dest.index) {
return false;
}
@@ -45,7 +45,7 @@ function DraggableSelectedList({ selected, onRemove, onRowDrag }) {
const newItems = reorder(selected, item.index, dest.index);
onRowDrag(newItems);
return true;
}
};
if (selected.length <= 0) {
return null;

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useState } from 'react';
import PropTypes from 'prop-types';

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import { arrayOf } from 'prop-types';
import { Link as _Link } from 'react-router-dom';

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import 'styled-components/macro';
import React from 'react';
import { t } from '@lingui/macro';

View File

@@ -133,14 +133,12 @@ function WorkflowNodeHelp({ node }) {
return (
<>
{!unifiedJobTemplate && (!job || job.type !== 'workflow_approval') && (
<>
<ResourceDeleted job={job}>
<StyledExclamationTriangleIcon />
<Trans>
The resource associated with this node has been deleted.
</Trans>
</ResourceDeleted>
</>
<ResourceDeleted job={job}>
<StyledExclamationTriangleIcon />
<Trans>
The resource associated with this node has been deleted.
</Trans>
</ResourceDeleted>
)}
{job && (
<GridDL>

View File

@@ -4,6 +4,7 @@ import React, {
useState,
useRef,
useCallback,
useMemo,
} from 'react';
import { useHistory, Redirect } from 'react-router-dom';
import { DateTime } from 'luxon';
@@ -163,23 +164,35 @@ function SessionProvider({ children }) {
clearInterval(sessionIntervalId.current);
}, []);
const sessionValue = useMemo(
() => ({
isUserBeingLoggedOut,
loginRedirectOverride,
authRedirectTo,
handleSessionContinue,
isSessionExpired,
logout,
sessionCountdown,
setAuthRedirectTo,
}),
[
isUserBeingLoggedOut,
loginRedirectOverride,
authRedirectTo,
handleSessionContinue,
isSessionExpired,
logout,
sessionCountdown,
setAuthRedirectTo,
]
);
if (isLoading) {
return null;
}
return (
<SessionContext.Provider
value={{
isUserBeingLoggedOut,
loginRedirectOverride,
authRedirectTo,
handleSessionContinue,
isSessionExpired,
logout,
sessionCountdown,
setAuthRedirectTo,
}}
>
<SessionContext.Provider value={sessionValue}>
{children}
</SessionContext.Provider>
);

View File

@@ -73,21 +73,19 @@ function ApplicationAdd({ onSuccessfulAdd }) {
return <ContentError error={error} />;
}
return (
<>
<PageSection>
<Card>
<CardBody>
<ApplicationForm
onSubmit={handleSubmit}
onCancel={handleCancel}
authorizationOptions={authorizationOptions}
clientTypeOptions={clientTypeOptions}
submitError={submitError}
/>
</CardBody>
</Card>
</PageSection>
</>
<PageSection>
<Card>
<CardBody>
<ApplicationForm
onSubmit={handleSubmit}
onCancel={handleCancel}
authorizationOptions={authorizationOptions}
clientTypeOptions={clientTypeOptions}
submitError={submitError}
/>
</CardBody>
</Card>
</PageSection>
);
}
export default ApplicationAdd;

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useState } from 'react';
import { useField, useFormikContext } from 'formik';
import { shape, string } from 'prop-types';

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useLocation, useHistory } from 'react-router-dom';

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useCallback, useEffect } from 'react';
import { useField, useFormikContext } from 'formik';

View File

@@ -74,59 +74,57 @@ function ExecutionEnvironmentTemplateList({ executionEnvironment }) {
}, [fetchTemplates]);
return (
<>
<Card>
<PaginatedTable
contentError={contentError}
hasContentLoading={isLoading}
items={templates}
itemCount={templatesCount}
pluralizedItemName={t`Templates`}
qsConfig={QS_CONFIG}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
toolbarSearchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
{
name: t`Type`,
key: 'or__type',
options: [
[`job_template`, t`Job Template`],
[`workflow_job_template`, t`Workflow Template`],
],
},
{
name: t`Created By (Username)`,
key: 'created_by__username__icontains',
},
{
name: t`Modified By (Username)`,
key: 'modified_by__username__icontains',
},
]}
renderToolbar={(props) => (
<DatalistToolbar {...props} qsConfig={QS_CONFIG} />
)}
headerRow={
<HeaderRow qsConfig={QS_CONFIG} isSelectable={false}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Type`}</HeaderCell>
</HeaderRow>
}
renderRow={(template) => (
<ExecutionEnvironmentTemplateListItem
key={template.id}
template={template}
detailUrl={`/templates/${template.type}/${template.id}/details`}
/>
)}
/>
</Card>
</>
<Card>
<PaginatedTable
contentError={contentError}
hasContentLoading={isLoading}
items={templates}
itemCount={templatesCount}
pluralizedItemName={t`Templates`}
qsConfig={QS_CONFIG}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
toolbarSearchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
{
name: t`Type`,
key: 'or__type',
options: [
[`job_template`, t`Job Template`],
[`workflow_job_template`, t`Workflow Template`],
],
},
{
name: t`Created By (Username)`,
key: 'created_by__username__icontains',
},
{
name: t`Modified By (Username)`,
key: 'modified_by__username__icontains',
},
]}
renderToolbar={(props) => (
<DatalistToolbar {...props} qsConfig={QS_CONFIG} />
)}
headerRow={
<HeaderRow qsConfig={QS_CONFIG} isSelectable={false}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Type`}</HeaderCell>
</HeaderRow>
}
renderRow={(template) => (
<ExecutionEnvironmentTemplateListItem
key={template.id}
template={template}
detailUrl={`/templates/${template.type}/${template.id}/details`}
/>
)}
/>
</Card>
);
}

View File

@@ -91,7 +91,7 @@ function ContainerGroup({ setBreadcrumb }) {
{contentError.response?.status === 404 && (
<span>
{t`Container group not found.`}
{''}
<Link to="/instance_groups">{t`View all instance groups`}</Link>
</span>
)}

View File

@@ -102,7 +102,7 @@ function InstanceGroup({ setBreadcrumb }) {
{contentError.response?.status === 404 && (
<span>
{t`Instance group not found.`}
{''}
<Link to="/instance_groups">{t`View all instance groups`}</Link>
</span>
)}

View File

@@ -28,9 +28,9 @@ const QS_CONFIG = getQSConfig('instance-group', {
});
function modifyInstanceGroups(
items = [],
defaultControlPlane,
defaultExecution
defaultExecution,
items = []
) {
return items.map((item) => {
const clonedItem = {
@@ -128,9 +128,9 @@ function InstanceGroupList({
useSelected(instanceGroups);
const modifiedSelected = modifyInstanceGroups(
selected,
defaultControlPlane,
defaultExecution
defaultExecution,
selected
);
const {
@@ -158,13 +158,10 @@ function InstanceGroupList({
const canAdd = actions && actions.POST;
function cannotDelete(item) {
return (
!item.summary_fields.user_capabilities.delete ||
item.name === defaultExecution ||
item.name === defaultControlPlane
);
}
const cannotDelete = (item) =>
!item.summary_fields.user_capabilities.delete ||
item.name === defaultExecution ||
item.name === defaultControlPlane;
const pluralizedItemName = t`Instance Groups`;

View File

@@ -107,104 +107,102 @@ function InventoryGroupsList() {
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
return (
<>
<PaginatedTable
contentError={contentError}
hasContentLoading={isLoading || isAdHocLaunchLoading}
items={groups}
itemCount={groupCount}
qsConfig={QS_CONFIG}
clearSelected={clearSelected}
toolbarSearchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
{
name: t`Group type`,
key: 'parents__isnull',
options: [['true', t`Show only root groups`]],
},
{
name: t`Created By (Username)`,
key: 'created_by__username__icontains',
},
{
name: t`Modified By (Username)`,
key: 'modified_by__username__icontains',
},
]}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Actions`}</HeaderCell>
</HeaderRow>
}
renderRow={(item, index) => (
<InventoryGroupItem
key={item.id}
group={item}
inventoryId={inventoryId}
isSelected={selected.some((row) => row.id === item.id)}
onSelect={() => handleSelect(item)}
rowIndex={index}
<PaginatedTable
contentError={contentError}
hasContentLoading={isLoading || isAdHocLaunchLoading}
items={groups}
itemCount={groupCount}
qsConfig={QS_CONFIG}
clearSelected={clearSelected}
toolbarSearchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
{
name: t`Group type`,
key: 'parents__isnull',
options: [['true', t`Show only root groups`]],
},
{
name: t`Created By (Username)`,
key: 'created_by__username__icontains',
},
{
name: t`Modified By (Username)`,
key: 'modified_by__username__icontains',
},
]}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Actions`}</HeaderCell>
</HeaderRow>
}
renderRow={(item, index) => (
<InventoryGroupItem
key={item.id}
group={item}
inventoryId={inventoryId}
isSelected={selected.some((row) => row.id === item.id)}
onSelect={() => handleSelect(item)}
rowIndex={index}
/>
)}
renderToolbar={(props) => (
<DataListToolbar
{...props}
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}
additionalControls={[
...(canAdd
? [
<ToolbarAddButton
key="add"
linkTo={`/inventories/inventory/${inventoryId}/groups/add`}
/>,
]
: []),
...(!isAdHocDisabled
? [
<AdHocCommands
adHocItems={selected}
hasListItems={groupCount > 0}
onLaunchLoading={setIsAdHocLaunchLoading}
moduleOptions={moduleOptions}
/>,
]
: []),
<Tooltip content={renderTooltip()} position="top" key="delete">
<div>
<InventoryGroupsDeleteModal
groups={selected}
isDisabled={
selected.length === 0 || selected.some(cannotDelete)
}
onAfterDelete={() => {
fetchData();
clearSelected();
}}
/>
</div>
</Tooltip>,
]}
/>
)}
emptyStateControls={
canAdd && (
<ToolbarAddButton
key="add"
linkTo={`/inventories/inventory/${inventoryId}/groups/add`}
/>
)}
renderToolbar={(props) => (
<DataListToolbar
{...props}
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}
additionalControls={[
...(canAdd
? [
<ToolbarAddButton
key="add"
linkTo={`/inventories/inventory/${inventoryId}/groups/add`}
/>,
]
: []),
...(!isAdHocDisabled
? [
<AdHocCommands
adHocItems={selected}
hasListItems={groupCount > 0}
onLaunchLoading={setIsAdHocLaunchLoading}
moduleOptions={moduleOptions}
/>,
]
: []),
<Tooltip content={renderTooltip()} position="top" key="delete">
<div>
<InventoryGroupsDeleteModal
groups={selected}
isDisabled={
selected.length === 0 || selected.some(cannotDelete)
}
onAfterDelete={() => {
fetchData();
clearSelected();
}}
/>
</div>
</Tooltip>,
]}
/>
)}
emptyStateControls={
canAdd && (
<ToolbarAddButton
key="add"
linkTo={`/inventories/inventory/${inventoryId}/groups/add`}
/>
)
}
/>
</>
)
}
/>
);
}
export default InventoryGroupsList;

View File

@@ -5,22 +5,20 @@ import InventoryRelatedGroupAdd from '../InventoryRelatedGroupAdd';
function InventoryRelatedGroups() {
return (
<>
<Switch>
<Route
key="addRelatedGroups"
path="/inventories/inventory/:id/groups/:groupId/nested_groups/add"
>
<InventoryRelatedGroupAdd />
</Route>
<Route
key="relatedGroups"
path="/inventories/inventory/:id/groups/:groupId/nested_groups"
>
<InventoryRelatedGroupList />
</Route>
</Switch>
</>
<Switch>
<Route
key="addRelatedGroups"
path="/inventories/inventory/:id/groups/:groupId/nested_groups/add"
>
<InventoryRelatedGroupAdd />
</Route>
<Route
key="relatedGroups"
path="/inventories/inventory/:id/groups/:groupId/nested_groups"
>
<InventoryRelatedGroupList />
</Route>
</Switch>
);
}
export default InventoryRelatedGroups;

View File

@@ -57,87 +57,85 @@ function InventorySourceListItem({
}
return (
<>
<Tr id={`source-row-${source.id}`} ouiaId={`source-row-${source.id}`}>
<Td
data-cy={`check-action-${source.id}`}
select={{
rowIndex,
isSelected,
onSelect,
}}
/>
<TdBreakWord dataLabel={t`Name`}>
<Link to={`${detailUrl}/details`}>
<b>{source.name}</b>
</Link>
{missingExecutionEnvironment && (
<span>
<Tooltip
className="missing-execution-environment"
content={t`Custom virtual environment ${source.custom_virtualenv} must be replaced by an execution environment.`}
position="right"
>
<ExclamationTriangleIcon />
</Tooltip>
</span>
)}
</TdBreakWord>
<Td dataLabel={t`Status`}>
{job && (
<Tr id={`source-row-${source.id}`} ouiaId={`source-row-${source.id}`}>
<Td
data-cy={`check-action-${source.id}`}
select={{
rowIndex,
isSelected,
onSelect,
}}
/>
<TdBreakWord dataLabel={t`Name`}>
<Link to={`${detailUrl}/details`}>
<b>{source.name}</b>
</Link>
{missingExecutionEnvironment && (
<span>
<Tooltip
position="top"
content={generateLastJobTooltip(job)}
key={job.id}
className="missing-execution-environment"
content={t`Custom virtual environment ${source.custom_virtualenv} must be replaced by an execution environment.`}
position="right"
>
<Link to={`/jobs/inventory/${job.id}`}>
<StatusLabel status={job.status} />
</Link>
<ExclamationTriangleIcon />
</Tooltip>
)}
</Td>
<Td dataLabel={t`Type`}>{label}</Td>
<ActionsTd dataLabel={t`Actions`}>
{['running', 'pending', 'waiting'].includes(job?.status) ? (
<ActionItem visible={source.summary_fields.user_capabilities.start}>
{source.summary_fields?.current_job?.id && (
<JobCancelButton
job={{
type: 'inventory_update',
id: source?.summary_fields?.current_job?.id,
}}
errorTitle={t`Inventory Source Sync Error`}
errorMessage={t`Failed to cancel Inventory Source Sync`}
title={t`Cancel Inventory Source Sync`}
showIconButton
/>
)}
</ActionItem>
) : (
<ActionItem
visible={source.summary_fields.user_capabilities.start}
tooltip={t`Sync`}
>
<InventorySourceSyncButton source={source} />
</ActionItem>
)}
<ActionItem
visible={source.summary_fields.user_capabilities.edit}
tooltip={t`Edit`}
</span>
)}
</TdBreakWord>
<Td dataLabel={t`Status`}>
{job && (
<Tooltip
position="top"
content={generateLastJobTooltip(job)}
key={job.id}
>
<Button
ouiaId={`${source.id}-edit-button`}
aria-label={t`Edit Source`}
variant="plain"
component={Link}
to={`${detailUrl}/edit`}
>
<PencilAltIcon />
</Button>
<Link to={`/jobs/inventory/${job.id}`}>
<StatusLabel status={job.status} />
</Link>
</Tooltip>
)}
</Td>
<Td dataLabel={t`Type`}>{label}</Td>
<ActionsTd dataLabel={t`Actions`}>
{['running', 'pending', 'waiting'].includes(job?.status) ? (
<ActionItem visible={source.summary_fields.user_capabilities.start}>
{source.summary_fields?.current_job?.id && (
<JobCancelButton
job={{
type: 'inventory_update',
id: source?.summary_fields?.current_job?.id,
}}
errorTitle={t`Inventory Source Sync Error`}
errorMessage={t`Failed to cancel Inventory Source Sync`}
title={t`Cancel Inventory Source Sync`}
showIconButton
/>
)}
</ActionItem>
</ActionsTd>
</Tr>
</>
) : (
<ActionItem
visible={source.summary_fields.user_capabilities.start}
tooltip={t`Sync`}
>
<InventorySourceSyncButton source={source} />
</ActionItem>
)}
<ActionItem
visible={source.summary_fields.user_capabilities.edit}
tooltip={t`Edit`}
>
<Button
ouiaId={`${source.id}-edit-button`}
aria-label={t`Edit Source`}
variant="plain"
component={Link}
to={`${detailUrl}/edit`}
>
<PencilAltIcon />
</Button>
</ActionItem>
</ActionsTd>
</Tr>
);
}
export default InventorySourceListItem;

View File

@@ -55,68 +55,66 @@ function SmartInventoryHostList({ inventory }) {
}, [fetchHosts]);
return (
<>
<PaginatedTable
contentError={contentError}
hasContentLoading={isLoading || isAdHocLaunchLoading}
items={hosts}
itemCount={count}
pluralizedItemName={t`Hosts`}
qsConfig={QS_CONFIG}
clearSelected={clearSelected}
toolbarSearchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
{
name: t`Created by (username)`,
key: 'created_by__username',
},
{
name: t`Modified by (username)`,
key: 'modified_by__username',
},
]}
renderToolbar={(props) => (
<DataListToolbar
{...props}
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}
additionalControls={
inventory?.summary_fields?.user_capabilities?.adhoc
? [
<AdHocCommands
adHocItems={selected}
hasListItems={count > 0}
onLaunchLoading={setIsAdHocLaunchLoading}
/>,
]
: []
}
/>
)}
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Recent jobs`}</HeaderCell>
<HeaderCell>{t`Inventory`}</HeaderCell>
</HeaderRow>
}
renderRow={(host, index) => (
<SmartInventoryHostListItem
key={host.id}
host={host}
detailUrl={`/inventories/smart_inventory/${inventory.id}/hosts/${host.id}/details`}
isSelected={selected.some((row) => row.id === host.id)}
onSelect={() => handleSelect(host)}
rowIndex={index}
/>
)}
/>
</>
<PaginatedTable
contentError={contentError}
hasContentLoading={isLoading || isAdHocLaunchLoading}
items={hosts}
itemCount={count}
pluralizedItemName={t`Hosts`}
qsConfig={QS_CONFIG}
clearSelected={clearSelected}
toolbarSearchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
{
name: t`Created by (username)`,
key: 'created_by__username',
},
{
name: t`Modified by (username)`,
key: 'modified_by__username',
},
]}
renderToolbar={(props) => (
<DataListToolbar
{...props}
isAllSelected={isAllSelected}
onSelectAll={selectAll}
qsConfig={QS_CONFIG}
additionalControls={
inventory?.summary_fields?.user_capabilities?.adhoc
? [
<AdHocCommands
adHocItems={selected}
hasListItems={count > 0}
onLaunchLoading={setIsAdHocLaunchLoading}
/>,
]
: []
}
/>
)}
headerRow={
<HeaderRow qsConfig={QS_CONFIG}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell>{t`Recent jobs`}</HeaderCell>
<HeaderCell>{t`Inventory`}</HeaderCell>
</HeaderRow>
}
renderRow={(host, index) => (
<SmartInventoryHostListItem
key={host.id}
host={host}
detailUrl={`/inventories/smart_inventory/${inventory.id}/hosts/${host.id}/details`}
isSelected={selected.some((row) => row.id === host.id)}
onSelect={() => handleSelect(host)}
rowIndex={index}
/>
)}
/>
);
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { t } from '@lingui/macro';

View File

@@ -42,58 +42,56 @@ const PageControls = ({
isFlatMode,
isTemplateJob,
}) => (
<>
<ControllsWrapper>
<ExpandCollapseWrapper>
{!isFlatMode && isTemplateJob && (
<Button
aria-label={
isAllCollapsed ? t`Expand job events` : t`Collapse all job events`
}
variant="plain"
type="button"
onClick={toggleExpandCollapseAll}
>
{isAllCollapsed ? <AngleRightIcon /> : <AngleDownIcon />}
</Button>
)}
</ExpandCollapseWrapper>
<ScrollWrapper>
<ControllsWrapper>
<ExpandCollapseWrapper>
{!isFlatMode && isTemplateJob && (
<Button
ouiaId="job-output-scroll-previous-button"
aria-label={t`Scroll previous`}
onClick={onScrollPrevious}
aria-label={
isAllCollapsed ? t`Expand job events` : t`Collapse all job events`
}
variant="plain"
type="button"
onClick={toggleExpandCollapseAll}
>
<AngleUpIcon />
{isAllCollapsed ? <AngleRightIcon /> : <AngleDownIcon />}
</Button>
<Button
ouiaId="job-output-scroll-next-button"
aria-label={t`Scroll next`}
onClick={onScrollNext}
variant="plain"
>
<AngleDownIcon />
</Button>
<Button
ouiaId="job-output-scroll-first-button"
aria-label={t`Scroll first`}
onClick={onScrollFirst}
variant="plain"
>
<AngleDoubleUpIcon />
</Button>
<Button
ouiaId="job-output-scroll-last-button"
aria-label={t`Scroll last`}
onClick={onScrollLast}
variant="plain"
>
<AngleDoubleDownIcon />
</Button>
</ScrollWrapper>
</ControllsWrapper>
</>
)}
</ExpandCollapseWrapper>
<ScrollWrapper>
<Button
ouiaId="job-output-scroll-previous-button"
aria-label={t`Scroll previous`}
onClick={onScrollPrevious}
variant="plain"
>
<AngleUpIcon />
</Button>
<Button
ouiaId="job-output-scroll-next-button"
aria-label={t`Scroll next`}
onClick={onScrollNext}
variant="plain"
>
<AngleDownIcon />
</Button>
<Button
ouiaId="job-output-scroll-first-button"
aria-label={t`Scroll first`}
onClick={onScrollFirst}
variant="plain"
>
<AngleDoubleUpIcon />
</Button>
<Button
ouiaId="job-output-scroll-last-button"
aria-label={t`Scroll last`}
onClick={onScrollLast}
variant="plain"
>
<AngleDoubleDownIcon />
</Button>
</ScrollWrapper>
</ControllsWrapper>
);
export default PageControls;

View File

@@ -68,10 +68,12 @@ const OutputToolbar = ({ job, onDelete, isDeleteDisabled, jobStatus }) => {
const taskCount = job?.playbook_counts?.task_count;
const darkCount = job?.host_status_counts?.dark;
const failureCount = job?.host_status_counts?.failures;
const totalHostCount = Object.keys(job?.host_status_counts || {}).reduce(
(sum, key) => sum + job?.host_status_counts[key],
0
);
const totalHostCount = job?.host_status_counts
? Object.keys(job.host_status_counts || {}).reduce(
(sum, key) => sum + job.host_status_counts[key],
0
)
: 0;
const { me } = useConfig();
return (

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/destructuring-assignment */
import { useState, useEffect, useReducer } from 'react';
const initialState = {

View File

@@ -9,6 +9,12 @@ import Job from './Job';
import JobTypeRedirect from './JobTypeRedirect';
import { JOB_TYPE_URL_SEGMENTS } from '../../constants';
function TypeRedirect({ view }) {
const { id } = useParams();
const { path } = useRouteMatch();
return <JobTypeRedirect id={id} path={path} view={view} />;
}
function Jobs() {
const match = useRouteMatch();
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
@@ -29,12 +35,6 @@ function Jobs() {
});
}, []);
function TypeRedirect({ view }) {
const { id } = useParams();
const { path } = useRouteMatch();
return <JobTypeRedirect id={id} path={path} view={view} />;
}
return (
<>
<ScreenHeader streamType="job" breadcrumbConfig={breadcrumbConfig} />

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useCallback, useEffect } from 'react';
import { Redirect, withRouter } from 'react-router-dom';
@@ -135,9 +136,9 @@ function AWXLogin({ alt, isAuthenticated }) {
/>
);
function setSessionRedirect() {
const setSessionRedirect = () => {
window.sessionStorage.setItem(SESSION_REDIRECT_URL, authRedirectTo);
}
};
return (
<Login header={Header} footer={Footer}>

View File

@@ -130,7 +130,7 @@ function ManagementJob({ setBreadcrumb }) {
{error?.response?.status === 404 && (
<span>
{t`Management job not found.`}
{''}
<Link to={basePath}>{t`View all management jobs`}</Link>
</span>
)}

View File

@@ -75,16 +75,14 @@ function ManagementJobListItem({
{isSuperUser ? (
<>
{isPrompted ? (
<>
<LaunchManagementPrompt
isOpen={isManagementPromptOpen}
isLoading={isManagementPromptLoading}
onClick={handleManagementPromptClick}
onClose={handleManagementPromptClose}
onConfirm={handleManagementPromptConfirm}
defaultDays={30}
/>
</>
<LaunchManagementPrompt
isOpen={isManagementPromptOpen}
isLoading={isManagementPromptLoading}
onClick={handleManagementPromptClick}
onClose={handleManagementPromptClose}
onConfirm={handleManagementPromptConfirm}
defaultDays={30}
/>
) : (
<Tooltip content={t`Launch management job`} position="top">
<Button

View File

@@ -120,7 +120,7 @@ function NotificationTemplateForm({
const messages = template.messages || { workflow_approval: {} };
const defs = defaultMessages[template.notification_type || 'email'];
const mergeDefaultMessages = (templ = {}, def) => ({
const mergeDefaultMessages = (def, templ = {}) => ({
message: templ?.message || def.message || '',
body: templ?.body || def.body || '',
});
@@ -140,32 +140,32 @@ function NotificationTemplateForm({
},
organization: template.summary_fields?.organization,
messages: {
started: { ...mergeDefaultMessages(messages.started, defs.started) },
success: { ...mergeDefaultMessages(messages.success, defs.success) },
error: { ...mergeDefaultMessages(messages.error, defs.error) },
started: { ...mergeDefaultMessages(defs.started, messages.started) },
success: { ...mergeDefaultMessages(defs.success, messages.success) },
error: { ...mergeDefaultMessages(defs.error, messages.error) },
workflow_approval: {
approved: {
...mergeDefaultMessages(
messages.workflow_approval?.approved,
defs.workflow_approval.approved
defs.workflow_approval.approved,
messages.workflow_approval?.approved
),
},
denied: {
...mergeDefaultMessages(
messages.workflow_approval?.denied,
defs.workflow_approval.denied
defs.workflow_approval.denied,
messages.workflow_approval?.denied
),
},
running: {
...mergeDefaultMessages(
messages.workflow_approval?.running,
defs.workflow_approval.running
defs.workflow_approval.running,
messages.workflow_approval?.running
),
},
timed_out: {
...mergeDefaultMessages(
messages.workflow_approval?.timed_out,
defs.workflow_approval.timed_out
defs.workflow_approval.timed_out,
messages.workflow_approval?.timed_out
),
},
},

View File

@@ -69,57 +69,55 @@ function OrganizationExecEnvList({ organization }) {
}, [fetchExecutionEnvironments]);
return (
<>
<Card>
<PaginatedTable
contentError={contentError}
hasContentLoading={isLoading}
items={executionEnvironments}
itemCount={executionEnvironmentsCount}
pluralizedItemName={t`Execution Environments`}
qsConfig={QS_CONFIG}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
toolbarSearchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
{
name: t`Image`,
key: 'image__icontains',
isDefault: false,
},
{
name: t`Created By (Username)`,
key: 'created_by__username__icontains',
},
{
name: t`Modified By (Username)`,
key: 'modified_by__username__icontains',
},
]}
renderToolbar={(props) => (
<DatalistToolbar {...props} qsConfig={QS_CONFIG} />
)}
headerRow={
<HeaderRow qsConfig={QS_CONFIG} isSelectable={false}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell sortKey="image">{t`Image`}</HeaderCell>
</HeaderRow>
}
renderRow={(executionEnvironment, index) => (
<OrganizationExecEnvListItem
key={executionEnvironment.id}
executionEnvironment={executionEnvironment}
detailUrl={`/execution_environments/${executionEnvironment.id}`}
rowIndex={index}
/>
)}
/>
</Card>
</>
<Card>
<PaginatedTable
contentError={contentError}
hasContentLoading={isLoading}
items={executionEnvironments}
itemCount={executionEnvironmentsCount}
pluralizedItemName={t`Execution Environments`}
qsConfig={QS_CONFIG}
toolbarSearchableKeys={searchableKeys}
toolbarRelatedSearchableKeys={relatedSearchableKeys}
toolbarSearchColumns={[
{
name: t`Name`,
key: 'name__icontains',
isDefault: true,
},
{
name: t`Image`,
key: 'image__icontains',
isDefault: false,
},
{
name: t`Created By (Username)`,
key: 'created_by__username__icontains',
},
{
name: t`Modified By (Username)`,
key: 'modified_by__username__icontains',
},
]}
renderToolbar={(props) => (
<DatalistToolbar {...props} qsConfig={QS_CONFIG} />
)}
headerRow={
<HeaderRow qsConfig={QS_CONFIG} isSelectable={false}>
<HeaderCell sortKey="name">{t`Name`}</HeaderCell>
<HeaderCell sortKey="image">{t`Image`}</HeaderCell>
</HeaderRow>
}
renderRow={(executionEnvironment, index) => (
<OrganizationExecEnvListItem
key={executionEnvironment.id}
executionEnvironment={executionEnvironment}
detailUrl={`/execution_environments/${executionEnvironment.id}`}
rowIndex={index}
/>
)}
/>
</Card>
);
}

View File

@@ -54,11 +54,11 @@ function SubscriptionModal({
const { selected, setSelected } = useSelected(subscriptions);
function handleConfirm() {
const handleConfirm = () => {
const [subscription] = selected;
onConfirm(subscription);
onClose();
}
};
useEffect(() => {
fetchSubscriptions();
@@ -109,29 +109,27 @@ function SubscriptionModal({
>
{isLoading && <ContentLoading />}
{!isLoading && error && (
<>
<EmptyState variant="full">
<EmptyStateIcon icon={ExclamationTriangleIcon} />
<Title size="lg" headingLevel="h3">
<Trans>No subscriptions found</Trans>
</Title>
<EmptyStateBody>
<Trans>
We were unable to locate licenses associated with this account.
</Trans>{' '}
<Button
aria-label={t`Close subscription modal`}
onClick={onClose}
variant="link"
isInline
ouiaId="subscription-modal-close"
>
<Trans>Return to subscription management.</Trans>
</Button>
</EmptyStateBody>
<ErrorDetail error={error} />
</EmptyState>
</>
<EmptyState variant="full">
<EmptyStateIcon icon={ExclamationTriangleIcon} />
<Title size="lg" headingLevel="h3">
<Trans>No subscriptions found</Trans>
</Title>
<EmptyStateBody>
<Trans>
We were unable to locate licenses associated with this account.
</Trans>{' '}
<Button
aria-label={t`Close subscription modal`}
onClick={onClose}
variant="link"
isInline
ouiaId="subscription-modal-close"
>
<Trans>Return to subscription management.</Trans>
</Button>
</EmptyStateBody>
<ErrorDetail error={error} />
</EmptyState>
)}
{!isLoading && !error && subscriptions?.length === 0 && (
<ContentEmpty

View File

@@ -119,36 +119,34 @@ function SubscriptionStep() {
labelIcon={
<Popover
content={
<>
<Trans>
A subscription manifest is an export of a Red Hat
Subscription. To generate a subscription manifest, go to{' '}
<Button
component="a"
href="https://access.redhat.com/management/subscription_allocations"
variant="link"
target="_blank"
isInline
ouiaId="subscription-allocations-link"
>
access.redhat.com
</Button>
. For more information, see the{' '}
<Button
component="a"
href={`${getDocsBaseUrl(
config
)}/html/userguide/import_license.html`}
variant="link"
target="_blank"
ouiaId="import-license-link"
isInline
>
User Guide
</Button>
.
</Trans>
</>
<Trans>
A subscription manifest is an export of a Red Hat
Subscription. To generate a subscription manifest, go to{' '}
<Button
component="a"
href="https://access.redhat.com/management/subscription_allocations"
variant="link"
target="_blank"
isInline
ouiaId="subscription-allocations-link"
>
access.redhat.com
</Button>
. For more information, see the{' '}
<Button
component="a"
href={`${getDocsBaseUrl(
config
)}/html/userguide/import_license.html`}
variant="link"
target="_blank"
ouiaId="import-license-link"
isInline
>
User Guide
</Button>
.
</Trans>
}
/>
}

View File

@@ -33,10 +33,10 @@ function RevertButton({
isMatch = true;
}
function handleConfirm() {
const handleConfirm = () => {
helpers.setValue(isRevertable ? defaultValue : initialValue);
onRevertCallback();
}
};
const revertTooltipContent = isRevertable
? t`Revert to factory default.`

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useState, useEffect } from 'react';
import { t } from '@lingui/macro';

View File

@@ -9,7 +9,7 @@ function JobTemplateAdd() {
const [formSubmitError, setFormSubmitError] = useState(null);
const history = useHistory();
async function handleSubmit(values) {
const handleSubmit = async (values) => {
const {
labels,
instanceGroups,
@@ -35,7 +35,7 @@ function JobTemplateAdd() {
execution_environment: values.execution_environment?.id,
});
await Promise.all([
submitLabels(id, labels, values.project.summary_fields.organization.id),
submitLabels(id, values.project.summary_fields.organization.id, labels),
submitInstanceGroups(id, instanceGroups),
submitCredentials(id, credentials),
]);
@@ -43,9 +43,9 @@ function JobTemplateAdd() {
} catch (error) {
setFormSubmitError(error);
}
}
};
async function submitLabels(templateId, labels = [], orgId) {
async function submitLabels(templateId, orgId, labels = []) {
if (!orgId) {
// eslint-disable-next-line no-useless-catch
try {
@@ -80,9 +80,9 @@ function JobTemplateAdd() {
return Promise.all(associateCredentials);
}
function handleCancel() {
const handleCancel = () => {
history.push(`/templates`);
}
};
return (
<PageSection>

View File

@@ -60,7 +60,7 @@ function JobTemplateEdit({ template, reloadTemplate }) {
try {
await JobTemplatesAPI.update(template.id, remainingValues);
await Promise.all([
submitLabels(labels, template?.organization),
submitLabels(template?.organization, labels),
submitCredentials(credentials),
JobTemplatesAPI.orderInstanceGroups(
template.id,
@@ -77,7 +77,7 @@ function JobTemplateEdit({ template, reloadTemplate }) {
}
};
const submitLabels = async (labels = [], orgId) => {
const submitLabels = async (orgId, labels = []) => {
const { added, removed } = getAddedAndRemoved(
template.summary_fields.labels.results,
labels

View File

@@ -36,14 +36,14 @@ function WorkflowJobTemplateAdd() {
const {
data: { id },
} = await WorkflowJobTemplatesAPI.create(templatePayload);
await Promise.all(await submitLabels(id, labels, organizationId));
await Promise.all(await submitLabels(id, organizationId, labels));
history.push(`/templates/workflow_job_template/${id}/visualizer`);
} catch (err) {
setFormSubmitError(err);
}
};
const submitLabels = async (templateId, labels = [], organizationId) => {
const submitLabels = async (templateId, organizationId, labels = []) => {
if (!organizationId) {
// eslint-disable-next-line no-useless-catch
try {

View File

@@ -34,7 +34,7 @@ function WorkflowJobTemplateEdit({ template }) {
organization?.id || inventory?.summary_fields?.organization.id || null;
try {
await Promise.all(
await submitLabels(labels, formOrgId, template.organization)
await submitLabels(formOrgId, template.organization, labels)
);
await WorkflowJobTemplatesAPI.update(template.id, templatePayload);
history.push(`/templates/workflow_job_template/${template.id}/details`);
@@ -43,7 +43,7 @@ function WorkflowJobTemplateEdit({ template }) {
}
};
const submitLabels = async (labels = [], formOrgId, templateOrgId) => {
const submitLabels = async (formOrgId, templateOrgId, labels = []) => {
const { added, removed } = getAddedAndRemoved(
template.summary_fields.labels.results,
labels

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import 'styled-components/macro';
import React, { useContext, useState, useEffect, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
@@ -118,11 +119,16 @@ function NodeModalForm({
contentError || credentialError
);
const nextButtonText = (activeStep) =>
activeStep.id === promptSteps[promptSteps?.length - 1]?.id ||
activeStep.name === 'Preview'
function nextButtonText(activeStep) {
let verifyPromptSteps = false;
if (promptSteps.length) {
verifyPromptSteps =
activeStep.id === promptSteps[promptSteps.length - 1]?.id;
}
return verifyPromptSteps || activeStep.name === 'Preview'
? t`Save`
: t`Next`;
}
const CustomFooter = (
<WizardFooter>

View File

@@ -72,9 +72,14 @@ function NodeViewModal({ readOnly }) {
fullUnifiedJobTemplate?.related?.webhook_receiver &&
!fullUnifiedJobTemplate.webhook_key
) {
const {
data: { webhook_key },
} = await nodeAPI?.readWebhookKey(fullUnifiedJobTemplate.id);
let webhook_key = null;
if (nodeAPI) {
const { data } = await nodeAPI.readWebhookKey(
fullUnifiedJobTemplate.id
);
webhook_key = data.webhook_key;
}
related.webhook_key = webhook_key;
}

View File

@@ -617,51 +617,49 @@ function JobTemplateForm({
</FormFullWidthLayout>
{(allowCallbacks || enableWebhooks) && (
<>
<SubFormLayout>
{allowCallbacks && (
<>
<Title size="md" headingLevel="h4">
{t`Provisioning Callback details`}
</Title>
<FormColumnLayout>
{callbackUrl && (
<FormGroup
label={t`Provisioning Callback URL`}
fieldId="template-callback-url"
>
<TextInput
id="template-callback-url"
isDisabled
value={callbackUrl}
/>
</FormGroup>
)}
<FormField
id="template-host-config-key"
name="host_config_key"
label={t`Host Config Key`}
validate={allowCallbacks ? required(null) : null}
isRequired={allowCallbacks}
/>
</FormColumnLayout>
</>
)}
<SubFormLayout>
{allowCallbacks && (
<>
<Title size="md" headingLevel="h4">
{t`Provisioning Callback details`}
</Title>
<FormColumnLayout>
{callbackUrl && (
<FormGroup
label={t`Provisioning Callback URL`}
fieldId="template-callback-url"
>
<TextInput
id="template-callback-url"
isDisabled
value={callbackUrl}
/>
</FormGroup>
)}
<FormField
id="template-host-config-key"
name="host_config_key"
label={t`Host Config Key`}
validate={allowCallbacks ? required(null) : null}
isRequired={allowCallbacks}
/>
</FormColumnLayout>
</>
)}
{allowCallbacks && enableWebhooks && <br />}
{allowCallbacks && enableWebhooks && <br />}
{enableWebhooks && (
<>
<Title size="md" headingLevel="h4">
{t`Webhook details`}
</Title>
<FormColumnLayout>
<WebhookSubForm templateType={template.type} />
</FormColumnLayout>
</>
)}
</SubFormLayout>
</>
{enableWebhooks && (
<>
<Title size="md" headingLevel="h4">
{t`Webhook details`}
</Title>
<FormColumnLayout>
<WebhookSubForm templateType={template.type} />
</FormColumnLayout>
</>
)}
</SubFormLayout>
)}
</FormColumnLayout>
</FormFullWidthLayout>

View File

@@ -50,12 +50,8 @@ function UserListItem({ user, isSelected, onSelect, detailUrl, rowIndex }) {
</span>
)}
</TdBreakWord>
<Td dataLabel={t`First Name`}>
{user.first_name && <>{user.first_name}</>}
</Td>
<Td dataLabel={t`Last Name`}>
{user.last_name && <>{user.last_name}</>}
</Td>
{user.first_name && <Td dataLabel={t`First Name`}>{user.first_name}</Td>}
{user.last_name && <Td dataLabel={t`Last Name`}>{user.last_name}</Td>}
<Td dataLabel={t`Role`}>{user_type}</Td>
<ActionsTd dataLabel={t`Actions`}>
<ActionItem

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useContext } from 'react';
import { t } from '@lingui/macro';

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React, { useContext } from 'react';
import { t } from '@lingui/macro';