mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 01:47:35 -02:30
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:
0
awx/ui/.babel.rc
Normal file
0
awx/ui/.babel.rc
Normal file
@@ -7,4 +7,5 @@ build
|
||||
node_modules
|
||||
dist
|
||||
images
|
||||
instrumented
|
||||
instrumented
|
||||
*test*.js
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
39260
awx/ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable default-param-last */
|
||||
import axios from 'axios';
|
||||
import { encodeQueryString } from 'util/qs';
|
||||
import debounce from 'util/debounce';
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useField } from 'formik';
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -35,9 +35,9 @@ export default function useCredentialsStep(
|
||||
validate: () => {
|
||||
helpers.setError(
|
||||
credentialsValidator(
|
||||
resourceDefaultCredentials,
|
||||
allowCredentialsWithPasswords,
|
||||
field.value
|
||||
field.value,
|
||||
resourceDefaultCredentials
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import 'styled-components/macro';
|
||||
import React from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useField, useFormikContext } from 'formik';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import { useState, useEffect, useReducer } from 'react';
|
||||
|
||||
const initialState = {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -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.`
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
Reference in New Issue
Block a user