mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 18:09:57 -03:30
Updates Lingui
This commit is contained in:
parent
b85559fe13
commit
43c8cabaa6
@ -93,7 +93,8 @@
|
||||
"RunOnRadio",
|
||||
"NodeTypeLetter",
|
||||
"SelectableItem",
|
||||
"Dash"
|
||||
"Dash",
|
||||
"Plural"
|
||||
],
|
||||
"ignoreCallee": ["describe"]
|
||||
}
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
{
|
||||
"localeDir": "src/locales/",
|
||||
"srcPathDirs": ["src/"],
|
||||
"format": "po",
|
||||
"sourceLocale": "en"
|
||||
{"catalogs":[{
|
||||
"path": "<rootDir>/locales/{locale}/messages",
|
||||
"include": ["<rootDir>"],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}],
|
||||
"compileNamespace": "cjs",
|
||||
"extractBabelOptions": {},
|
||||
"compilerBabelOptions": {},
|
||||
"fallbackLocales": { default: "en"},
|
||||
"format": "po",
|
||||
"locales": ["en","es","fr","nl","zh","ja", "zu"],
|
||||
"orderBy": "messageId",
|
||||
"pseudoLocale": "zu",
|
||||
"rootDir": "./src",
|
||||
"runtimeConfigModule": ["@lingui/core", "i18n"],
|
||||
"sourceLocale": "en",
|
||||
}
|
||||
|
||||
1261
awx/ui_next/package-lock.json
generated
1261
awx/ui_next/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@
|
||||
"node": "14.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lingui/react": "^2.9.1",
|
||||
"@lingui/react": "^3.7.1",
|
||||
"@patternfly/patternfly": "^4.80.3",
|
||||
"@patternfly/react-core": "^4.90.2",
|
||||
"@patternfly/react-icons": "4.7.22",
|
||||
@ -14,6 +14,8 @@
|
||||
"ace-builds": "^1.4.12",
|
||||
"ansi-to-html": "^0.6.11",
|
||||
"axios": "^0.21.1",
|
||||
"babel-plugin-macros": "^3.0.1",
|
||||
"codemirror": "^5.47.0",
|
||||
"d3": "^5.12.0",
|
||||
"dagre": "^0.8.4",
|
||||
"formik": "^2.1.2",
|
||||
@ -33,8 +35,8 @@
|
||||
"devDependencies": {
|
||||
"@babel/polyfill": "^7.8.7",
|
||||
"@cypress/instrument-cra": "^1.4.0",
|
||||
"@lingui/cli": "^2.9.2",
|
||||
"@lingui/macro": "^2.9.1",
|
||||
"@lingui/cli": "^3.7.1",
|
||||
"@lingui/macro": "^3.7.1",
|
||||
"@nteract/mockument": "^1.0.4",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"enzyme": "^3.10.0",
|
||||
|
||||
@ -7,18 +7,24 @@ import {
|
||||
Switch,
|
||||
Redirect,
|
||||
} from 'react-router-dom';
|
||||
import { I18n, I18nProvider } from '@lingui/react';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { i18n } from '@lingui/core';
|
||||
|
||||
import { ConfigProvider, useAuthorizedPath } from './contexts/Config';
|
||||
import { en, es, fr, nl, zh, ja, zu } from 'make-plural/plurals';
|
||||
import AppContainer from './components/AppContainer';
|
||||
import Background from './components/Background';
|
||||
import NotFound from './screens/NotFound';
|
||||
import Login from './screens/Login';
|
||||
|
||||
import ja from './locales/ja/messages';
|
||||
import en from './locales/en/messages';
|
||||
import japanese from './locales/ja/messages';
|
||||
import english from './locales/en/messages';
|
||||
import zulu from './locales/zu/messages';
|
||||
import french from './locales/fr/messages';
|
||||
import dutch from './locales/nl/messages';
|
||||
import chinese from './locales/zh/messages';
|
||||
import spanish from './locales/es/messages';
|
||||
import { isAuthenticated } from './util/auth';
|
||||
|
||||
import { getLanguageWithoutRegionCode } from './util/language';
|
||||
|
||||
import getRouteConfig from './routeConfig';
|
||||
@ -74,7 +80,15 @@ const ProtectedRoute = ({ children, ...rest }) =>
|
||||
);
|
||||
|
||||
function App() {
|
||||
const catalogs = { en, ja };
|
||||
const catalogs = {
|
||||
en: english,
|
||||
ja: japanese,
|
||||
zu: zulu,
|
||||
fr: french,
|
||||
es: spanish,
|
||||
zh: chinese,
|
||||
nl: dutch,
|
||||
};
|
||||
let language = getLanguageWithoutRegionCode(navigator);
|
||||
if (!Object.keys(catalogs).includes(language)) {
|
||||
// If there isn't a string catalog available for the browser's
|
||||
@ -83,32 +97,56 @@ function App() {
|
||||
}
|
||||
const { hash, search, pathname } = useLocation();
|
||||
|
||||
i18n.loadLocaleData('en', { plurals: en });
|
||||
i18n.loadLocaleData('es', { plurals: es });
|
||||
i18n.loadLocaleData('fr', { plurals: fr });
|
||||
i18n.loadLocaleData('nl', { plurals: nl });
|
||||
i18n.loadLocaleData('zh', { plurals: zh });
|
||||
i18n.loadLocaleData('ja', { plurals: ja });
|
||||
i18n.loadLocaleData('zu', { plurals: zu });
|
||||
i18n.load({
|
||||
en: english.messages,
|
||||
ja: japanese.messages,
|
||||
zu: zulu.messages,
|
||||
fr: french.messages,
|
||||
nl: dutch.messages,
|
||||
zh: chinese.messages,
|
||||
es: spanish.messages,
|
||||
});
|
||||
i18n.activate(language);
|
||||
return (
|
||||
<I18nProvider language={language} catalogs={catalogs}>
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Background>
|
||||
<Switch>
|
||||
<Route exact strict path="/*/">
|
||||
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
||||
</Route>
|
||||
<Route path="/login">
|
||||
<Login isAuthenticated={isAuthenticated} />
|
||||
</Route>
|
||||
<Route exact path="/">
|
||||
<Redirect to="/home" />
|
||||
</Route>
|
||||
<ProtectedRoute>
|
||||
<ConfigProvider>
|
||||
<AppContainer navRouteConfig={getRouteConfig(i18n)}>
|
||||
<AuthorizedRoutes routeConfig={getRouteConfig(i18n)} />
|
||||
</AppContainer>
|
||||
</ConfigProvider>
|
||||
</ProtectedRoute>
|
||||
</Switch>
|
||||
</Background>
|
||||
)}
|
||||
</I18n>
|
||||
<I18nProvider i18n={i18n} catalogs={catalogs}>
|
||||
<Background>
|
||||
<Switch>
|
||||
<Route exact strict path="/*/">
|
||||
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
||||
</Route>
|
||||
<Route path="/login">
|
||||
<Login isAuthenticated={isAuthenticated} />
|
||||
</Route>
|
||||
<Route exact path="/">
|
||||
<Redirect to="/home" />
|
||||
</Route>
|
||||
<ProtectedRoute>
|
||||
<AppContainer navRouteConfig={getRouteConfig(i18n)}>
|
||||
<Switch>
|
||||
{getRouteConfig(i18n)
|
||||
.flatMap(({ routes }) => routes)
|
||||
.map(({ path, screen: Screen }) => (
|
||||
<ProtectedRoute key={path} path={path}>
|
||||
<Screen match={match} />
|
||||
</ProtectedRoute>
|
||||
))
|
||||
.concat(
|
||||
<ProtectedRoute key="not-found" path="*">
|
||||
<NotFound />
|
||||
</ProtectedRoute>
|
||||
)}
|
||||
</Switch>
|
||||
</AppContainer>
|
||||
</ProtectedRoute>
|
||||
</Switch>
|
||||
</Background>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import {
|
||||
mountWithContexts,
|
||||
shallowWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import { sleep } from '../../../testUtils/testUtils';
|
||||
@ -27,17 +27,19 @@ describe('<SelectResourceStep />', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
test('initially renders without crashing', () => {
|
||||
shallow(
|
||||
<SelectResourceStep
|
||||
searchColumns={searchColumns}
|
||||
sortColumns={sortColumns}
|
||||
displayKey="username"
|
||||
onRowClick={() => {}}
|
||||
fetchItems={() => {}}
|
||||
fetchOptions={() => {}}
|
||||
/>
|
||||
);
|
||||
test('initially renders without crashing', async () => {
|
||||
act(() => {
|
||||
shallowWithContexts(
|
||||
<SelectResourceStep
|
||||
searchColumns={searchColumns}
|
||||
sortColumns={sortColumns}
|
||||
displayKey="username"
|
||||
onRowClick={() => {}}
|
||||
fetchItems={() => {}}
|
||||
fetchOptions={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('fetches resources on mount and adds items to list', async () => {
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
|
||||
import {
|
||||
mountWithContexts,
|
||||
shallowWithContexts,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
|
||||
import SelectRoleStep from './SelectRoleStep';
|
||||
|
||||
describe('<SelectRoleStep />', () => {
|
||||
@ -31,7 +35,7 @@ describe('<SelectRoleStep />', () => {
|
||||
},
|
||||
];
|
||||
test('initially renders without crashing', () => {
|
||||
wrapper = shallow(
|
||||
wrapper = shallowWithContexts(
|
||||
<SelectRoleStep
|
||||
roles={roles}
|
||||
selectedResourceRows={selectedResourceRows}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { t, Plural } from '@lingui/macro';
|
||||
import { arrayOf, func } from 'prop-types';
|
||||
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||
import { KebabifiedContext } from '../../contexts/Kebabified';
|
||||
@ -22,7 +22,6 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
|
||||
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const numJobsToCancel = jobsToCancel.length;
|
||||
const zeroOrOneJobSelected = numJobsToCancel < 2;
|
||||
|
||||
const handleCancelJob = () => {
|
||||
onCancel();
|
||||
@ -54,35 +53,32 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
|
||||
<div>
|
||||
{cannotCancelPermissions.length > 0 && (
|
||||
<div>
|
||||
{i18n._(
|
||||
'{numJobsUnableToCancel, plural, one {You do not have permission to cancel the following job:} other {You do not have permission to cancel the following jobs:}}',
|
||||
{
|
||||
numJobsUnableToCancel: cannotCancelPermissions.length,
|
||||
}
|
||||
)}
|
||||
{' '.concat(cannotCancelPermissions.join(', '))}
|
||||
<Plural
|
||||
value={cannotCancelPermissions.length}
|
||||
one="You do not have permission to cancel the following job:"
|
||||
other="You do not have permission to cancel the following jobs:"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{cannotCancelNotRunning.length > 0 && (
|
||||
<div>
|
||||
{i18n._(
|
||||
'{numJobsUnableToCancel, plural, one {You cannot cancel the following job because it is not running:} other {You cannot cancel the following jobs because they are not running:}}',
|
||||
{
|
||||
numJobsUnableToCancel: cannotCancelNotRunning.length,
|
||||
}
|
||||
)}
|
||||
{' '.concat(cannotCancelNotRunning.join(', '))}
|
||||
<Plural
|
||||
value={cannotCancelNotRunning.length}
|
||||
one="You cannot cancel the following job because it is not running"
|
||||
other="You cannot cancel the following jobs because they are not running"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (numJobsToCancel > 0) {
|
||||
return i18n._(
|
||||
'{numJobsToCancel, plural, one {Cancel selected job} other {Cancel selected jobs}}',
|
||||
{
|
||||
numJobsToCancel,
|
||||
}
|
||||
return (
|
||||
<Plural
|
||||
value={numJobsToCancel}
|
||||
one={i18n._(t`Cancel selected job`)}
|
||||
other={i18n._(t`Cancel selected jobs`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return i18n._(t`Select a job to cancel`);
|
||||
@ -92,12 +88,8 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
|
||||
jobsToCancel.length === 0 ||
|
||||
jobsToCancel.some(cannotCancelBecausePermissions) ||
|
||||
jobsToCancel.some(cannotCancelBecauseNotRunning);
|
||||
|
||||
const cancelJobText = i18n._(
|
||||
'{zeroOrOneJobSelected, plural, one {Cancel job} other {Cancel jobs}}',
|
||||
{
|
||||
zeroOrOneJobSelected,
|
||||
}
|
||||
const cancelJobText = (
|
||||
<Plural value={numJobsToCancel} one="Cancel job" other="Cancel jobs" />
|
||||
);
|
||||
|
||||
return (
|
||||
@ -156,12 +148,11 @@ function JobListCancelButton({ i18n, jobsToCancel, onCancel }) {
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
{i18n._(
|
||||
'{numJobsToCancel, plural, one {This action will cancel the following job:} other {This action will cancel the following jobs:}}',
|
||||
{
|
||||
numJobsToCancel,
|
||||
}
|
||||
)}
|
||||
<Plural
|
||||
value={numJobsToCancel}
|
||||
one="This action will cancel the following job:"
|
||||
other="This action will cancel the following jobs:"
|
||||
/>
|
||||
</div>
|
||||
{jobsToCancel.map(job => (
|
||||
<span key={job.id}>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,7 @@ import {
|
||||
Pagination as PFPagination,
|
||||
DropdownDirection,
|
||||
} from '@patternfly/react-core';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
const AWXPagination = styled(PFPagination)`
|
||||
@ -19,26 +19,22 @@ const AWXPagination = styled(PFPagination)`
|
||||
`;
|
||||
|
||||
export default props => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<AWXPagination
|
||||
titles={{
|
||||
items: i18n._(t`items`),
|
||||
page: i18n._(t`page`),
|
||||
pages: i18n._(t`pages`),
|
||||
itemsPerPage: i18n._(t`Items per page`),
|
||||
perPageSuffix: i18n._(t`per page`),
|
||||
toFirstPage: i18n._(t`Go to first page`),
|
||||
toPreviousPage: i18n._(t`Go to previous page`),
|
||||
toLastPage: i18n._(t`Go to last page`),
|
||||
toNextPage: i18n._(t`Go to next page`),
|
||||
optionsToggle: i18n._(t`Select`),
|
||||
currPage: i18n._(t`Current page`),
|
||||
paginationTitle: i18n._(t`Pagination`),
|
||||
}}
|
||||
dropDirection={DropdownDirection.up}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</I18n>
|
||||
<AWXPagination
|
||||
titles={{
|
||||
items: i18n._(t`items`),
|
||||
page: i18n._(t`page`),
|
||||
pages: i18n._(t`pages`),
|
||||
itemsPerPage: i18n._(t`Items per page`),
|
||||
perPageSuffix: i18n._(t`per page`),
|
||||
toFirstPage: i18n._(t`Go to first page`),
|
||||
toPreviousPage: i18n._(t`Go to previous page`),
|
||||
toLastPage: i18n._(t`Go to last page`),
|
||||
toNextPage: i18n._(t`Go to next page`),
|
||||
optionsToggle: i18n._(t`Select`),
|
||||
currPage: i18n._(t`Current page`),
|
||||
paginationTitle: i18n._(t`Pagination`),
|
||||
}}
|
||||
dropDirection={DropdownDirection.up}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -15,12 +15,12 @@ function DeleteRoleConfirmationModal({
|
||||
i18n,
|
||||
}) {
|
||||
const isTeamRole = () => {
|
||||
return typeof role.team_id !== 'undefined';
|
||||
return typeof role.team_id !== 'undefined'
|
||||
? i18n._(t`Team`)
|
||||
: i18n._(t`User`);
|
||||
};
|
||||
|
||||
const title = i18n._(
|
||||
t`Remove ${isTeamRole() ? i18n._(t`Team`) : i18n._(t`User`)} Access`
|
||||
);
|
||||
const title = i18n._(t`Remove ${isTeamRole()} Access`);
|
||||
return (
|
||||
<AlertModal
|
||||
variant="danger"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useField } from 'formik';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { t, Trans, Plural } from '@lingui/macro';
|
||||
import { RRule } from 'rrule';
|
||||
import {
|
||||
Checkbox as _Checkbox,
|
||||
@ -185,29 +185,17 @@ const FrequencyDetailSubform = ({ i18n }) => {
|
||||
|
||||
switch (frequency.value) {
|
||||
case 'minute':
|
||||
return i18n._('{intervalValue, plural, one {minute} other {minutes}}', {
|
||||
intervalValue,
|
||||
});
|
||||
return <Plural value={intervalValue} one="minute" other="minutes" />;
|
||||
case 'hour':
|
||||
return i18n._('{intervalValue, plural, one {hour} other {hours}}', {
|
||||
intervalValue,
|
||||
});
|
||||
return <Plural value={intervalValue} one="hour" other="hours" />;
|
||||
case 'day':
|
||||
return i18n._('{intervalValue, plural, one {day} other {days}}', {
|
||||
intervalValue,
|
||||
});
|
||||
return <Plural value={intervalValue} one="day" other="days" />;
|
||||
case 'week':
|
||||
return i18n._('{intervalValue, plural, one {week} other {weeks}}', {
|
||||
intervalValue,
|
||||
});
|
||||
return <Plural value={intervalValue} one="week" other="weeks" />;
|
||||
case 'month':
|
||||
return i18n._('{intervalValue, plural, one {month} other {months}}', {
|
||||
intervalValue,
|
||||
});
|
||||
return <Plural value={intervalValue} one="month" other="months" />;
|
||||
case 'year':
|
||||
return i18n._('{intervalValue, plural, one {year} other {years}}', {
|
||||
intervalValue,
|
||||
});
|
||||
return <Plural value={intervalValue} one="year" other="years" />;
|
||||
default:
|
||||
throw new Error(i18n._(t`Frequency did not match an expected value`));
|
||||
}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
|
||||
import {
|
||||
mountWithContexts,
|
||||
shallowWithContexts,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
|
||||
import Sparkline from './Sparkline';
|
||||
|
||||
describe('Sparkline', () => {
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mount(<Sparkline />);
|
||||
const wrapper = shallowWithContexts(<Sparkline />);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
test('renders an icon with tooltips and links for each job', () => {
|
||||
|
||||
@ -170,8 +170,7 @@ function TemplateList({ defaultParams, i18n }) {
|
||||
);
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
|
||||
selected[0],
|
||||
i18n
|
||||
selected[0]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import { en } from 'make-plural/plurals';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import english from '../../locales/en/messages';
|
||||
import { WorkflowStateContext } from '../../contexts/Workflow';
|
||||
import WorkflowStartNode from './WorkflowStartNode';
|
||||
|
||||
@ -10,16 +15,22 @@ const nodePositions = {
|
||||
},
|
||||
};
|
||||
|
||||
i18n.loadLocaleData({ en: { plurals: en } });
|
||||
i18n.load({ en: english });
|
||||
i18n.activate('en');
|
||||
|
||||
describe('WorkflowStartNode', () => {
|
||||
test('mounts successfully', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<svg>
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowStartNode
|
||||
nodePositions={nodePositions}
|
||||
showActionTooltip={false}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowStartNode
|
||||
nodePositions={nodePositions}
|
||||
showActionTooltip={false}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</I18nProvider>
|
||||
</svg>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
@ -27,9 +38,14 @@ describe('WorkflowStartNode', () => {
|
||||
test('tooltip shown on hover', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<svg>
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowStartNode nodePositions={nodePositions} showActionTooltip />
|
||||
</WorkflowStateContext.Provider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<WorkflowStateContext.Provider value={{ nodePositions }}>
|
||||
<WorkflowStartNode
|
||||
nodePositions={nodePositions}
|
||||
showActionTooltip
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</I18nProvider>
|
||||
</svg>
|
||||
);
|
||||
expect(wrapper.find('WorkflowActionTooltip')).toHaveLength(0);
|
||||
|
||||
@ -185,8 +185,7 @@ function CredentialDetail({ i18n, credential }) {
|
||||
}, [fetchDetails]);
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.credential(
|
||||
credential,
|
||||
i18n
|
||||
credential
|
||||
);
|
||||
|
||||
if (hasContentLoading) {
|
||||
|
||||
@ -105,8 +105,7 @@ function CredentialList({ i18n }) {
|
||||
const canAdd =
|
||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.credential(
|
||||
selected[0],
|
||||
i18n
|
||||
selected[0]
|
||||
);
|
||||
return (
|
||||
<PageSection>
|
||||
|
||||
@ -107,8 +107,7 @@ function CredentialTypeList({ i18n }) {
|
||||
const canAdd = actions && actions.POST;
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.credentialType(
|
||||
selected[0],
|
||||
i18n
|
||||
selected[0]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -43,8 +43,7 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) {
|
||||
|
||||
const { error, dismissError } = useDismissableError(deleteError);
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.executionEnvironment(
|
||||
executionEnvironment,
|
||||
i18n
|
||||
executionEnvironment
|
||||
);
|
||||
return (
|
||||
<CardBody>
|
||||
|
||||
@ -106,8 +106,7 @@ function ExecutionEnvironmentList({ i18n }) {
|
||||
|
||||
const canAdd = actions && actions.POST;
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.executionEnvironment(
|
||||
selected[0],
|
||||
i18n
|
||||
selected[0]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -36,8 +36,7 @@ function ContainerGroupDetails({ instanceGroup, i18n }) {
|
||||
|
||||
const { error, dismissError } = useDismissableError(deleteError);
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
|
||||
instanceGroup,
|
||||
i18n
|
||||
instanceGroup
|
||||
);
|
||||
return (
|
||||
<CardBody>
|
||||
|
||||
@ -40,8 +40,7 @@ function InstanceGroupDetails({ instanceGroup, i18n }) {
|
||||
|
||||
const { error, dismissError } = useDismissableError(deleteError);
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
|
||||
instanceGroup,
|
||||
i18n
|
||||
instanceGroup
|
||||
);
|
||||
const verifyInstanceGroup = item => {
|
||||
if (item.is_isolated) {
|
||||
|
||||
@ -187,8 +187,7 @@ function InstanceGroupList({ i18n }) {
|
||||
: `${match.url}/${item.id}/details`;
|
||||
};
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.instanceGroup(
|
||||
selected[0],
|
||||
i18n
|
||||
selected[0]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -56,8 +56,7 @@ function InventoryDetail({ inventory, i18n }) {
|
||||
} = inventory.summary_fields;
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(
|
||||
inventory,
|
||||
i18n
|
||||
inventory
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
@ -219,10 +219,12 @@ describe('<InventoryGroupsList/> error handling', () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')();
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'AlertModal[title="Delete Group?"]',
|
||||
el => el.props().isOpen === true
|
||||
'AlertModal__Header',
|
||||
el => el.text() === 'Delete Group?'
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper.find('Radio[id="radio-delete"]').invoke('onChange')();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useLocation, useRouteMatch, Link } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { t, Plural } from '@lingui/macro';
|
||||
import { Card, PageSection, DropdownItem } from '@patternfly/react-core';
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||
@ -129,8 +129,7 @@ function InventoryList({ i18n }) {
|
||||
};
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(
|
||||
selected[0],
|
||||
i18n
|
||||
selected[0]
|
||||
);
|
||||
|
||||
const addInventory = i18n._(t`Add inventory`);
|
||||
@ -219,15 +218,14 @@ function InventoryList({ i18n }) {
|
||||
onDelete={handleInventoryDelete}
|
||||
itemsToDelete={selected}
|
||||
pluralizedItemName={i18n._(t`Inventories`)}
|
||||
warningMessage={i18n._(
|
||||
'{numItemsToDelete, plural, one {The inventory will be in a pending status until the final delete is processed.} other {The inventories will be in a pending status until the final delete is processed.}}',
|
||||
{ numItemsToDelete: selected.length }
|
||||
)}
|
||||
deleteMessage={i18n._(
|
||||
'{numItemsToDelete, plural, one {This inventory is currently being used by other resources. Are you sure you want to delete it?} other {Deleting these inventories could impact other resources that rely on them. Are you sure you want to delete anyway?}}',
|
||||
{ numItemsToDelete: selected.length }
|
||||
)}
|
||||
deleteDetailsRequests={deleteDetailsRequests}
|
||||
warningMessage={
|
||||
<Plural
|
||||
value={selected.length}
|
||||
one="The inventory will be in a pending status until the final delete is processed."
|
||||
other="The inventories will be in a pending status until the final delete is processed."
|
||||
/>
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
|
||||
@ -99,7 +99,6 @@ function InventorySourceDetail({ inventorySource, i18n }) {
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.inventorySource(
|
||||
inventorySource.inventory,
|
||||
i18n,
|
||||
inventorySource
|
||||
);
|
||||
|
||||
|
||||
@ -146,7 +146,6 @@ function InventorySourceList({ i18n }) {
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.inventorySource(
|
||||
id,
|
||||
i18n,
|
||||
selected[0]
|
||||
);
|
||||
return (
|
||||
|
||||
@ -2,8 +2,8 @@ import 'styled-components/macro';
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { func, bool, arrayOf } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { t, Plural } from '@lingui/macro';
|
||||
import { Button, Radio, DropdownItem } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
import { KebabifiedContext } from '../../../contexts/Kebabified';
|
||||
@ -18,12 +18,8 @@ const ListItem = styled.li`
|
||||
color: var(--pf-global--danger-color--100);
|
||||
`;
|
||||
|
||||
const InventoryGroupsDeleteModal = ({
|
||||
onAfterDelete,
|
||||
isDisabled,
|
||||
groups,
|
||||
i18n,
|
||||
}) => {
|
||||
const InventoryGroupsDeleteModal = ({ onAfterDelete, isDisabled, groups }) => {
|
||||
const { i18n } = useLingui();
|
||||
const [radioOption, setRadioOption] = useState(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
@ -87,9 +83,11 @@ const InventoryGroupsDeleteModal = ({
|
||||
isOpen={isModalOpen}
|
||||
variant="danger"
|
||||
title={
|
||||
groups.length > 1
|
||||
? i18n._(t`Delete Groups?`)
|
||||
: i18n._(t`Delete Group?`)
|
||||
<Plural
|
||||
value={groups.length}
|
||||
one="Delete Group?"
|
||||
other="Delete Groups?"
|
||||
/>
|
||||
}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
actions={[
|
||||
@ -112,11 +110,12 @@ const InventoryGroupsDeleteModal = ({
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{i18n._(
|
||||
t`Are you sure you want to delete the ${
|
||||
groups.length > 1 ? i18n._(t`groups`) : i18n._(t`group`)
|
||||
} below?`
|
||||
)}
|
||||
<Plural
|
||||
value={groups.length}
|
||||
one="Are you sure you want delete the group below?"
|
||||
other="Are you sure you want delete the groups below?"
|
||||
/>
|
||||
|
||||
<div css="padding: 24px 0;">
|
||||
{groups.map(group => {
|
||||
return <ListItem key={group.id}>{group.name}</ListItem>;
|
||||
@ -167,4 +166,4 @@ InventoryGroupsDeleteModal.defaultProps = {
|
||||
groups: [],
|
||||
};
|
||||
|
||||
export default withI18n()(InventoryGroupsDeleteModal);
|
||||
export default InventoryGroupsDeleteModal;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useHistory, useLocation, withRouter } from 'react-router-dom';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
@ -625,7 +625,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
||||
history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname);
|
||||
};
|
||||
|
||||
const renderSearchComponent = i18n => (
|
||||
const renderSearchComponent = () => (
|
||||
<Search
|
||||
qsConfig={QS_CONFIG}
|
||||
columns={[
|
||||
@ -688,176 +688,157 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<>
|
||||
<CardBody>
|
||||
{isHostModalOpen && (
|
||||
<HostEventModal
|
||||
onClose={handleHostModalClose}
|
||||
isOpen={isHostModalOpen}
|
||||
hostEvent={hostEvent}
|
||||
/>
|
||||
)}
|
||||
<OutputHeader>
|
||||
<HeaderTitle>
|
||||
<StatusIcon status={job.status} />
|
||||
<h1>{job.name}</h1>
|
||||
</HeaderTitle>
|
||||
<OutputToolbar
|
||||
job={job}
|
||||
jobStatus={jobStatus}
|
||||
onCancel={() => setShowCancelModal(true)}
|
||||
onDelete={deleteJob}
|
||||
isDeleteDisabled={isDeleting}
|
||||
/>
|
||||
</OutputHeader>
|
||||
<HostStatusBar counts={job.host_status_counts} />
|
||||
<SearchToolbar
|
||||
id="job_output-toolbar"
|
||||
clearAllFilters={handleRemoveAllSearchTerms}
|
||||
collapseListedFiltersBreakpoint="lg"
|
||||
clearFiltersButtonText={i18n._(t`Clear all filters`)}
|
||||
>
|
||||
<SearchToolbarContent>
|
||||
<ToolbarToggleGroup toggleIcon={<SearchIcon />} breakpoint="lg">
|
||||
<ToolbarItem variant="search-filter">
|
||||
{isJobRunning(job.status) ? (
|
||||
<Tooltip
|
||||
content={i18n._(
|
||||
t`Search is disabled while the job is running`
|
||||
)}
|
||||
>
|
||||
{renderSearchComponent(i18n)}
|
||||
</Tooltip>
|
||||
) : (
|
||||
renderSearchComponent(i18n)
|
||||
<>
|
||||
<CardBody>
|
||||
{isHostModalOpen && (
|
||||
<HostEventModal
|
||||
onClose={handleHostModalClose}
|
||||
isOpen={isHostModalOpen}
|
||||
hostEvent={hostEvent}
|
||||
/>
|
||||
)}
|
||||
<OutputHeader>
|
||||
<HeaderTitle>
|
||||
<StatusIcon status={job.status} />
|
||||
<h1>{job.name}</h1>
|
||||
</HeaderTitle>
|
||||
<OutputToolbar
|
||||
job={job}
|
||||
jobStatus={jobStatus}
|
||||
onCancel={() => setShowCancelModal(true)}
|
||||
onDelete={deleteJob}
|
||||
isDeleteDisabled={isDeleting}
|
||||
/>
|
||||
</OutputHeader>
|
||||
<HostStatusBar counts={job.host_status_counts} />
|
||||
<SearchToolbar
|
||||
id="job_output-toolbar"
|
||||
clearAllFilters={handleRemoveAllSearchTerms}
|
||||
collapseListedFiltersBreakpoint="lg"
|
||||
clearFiltersButtonText={i18n._(t`Clear all filters`)}
|
||||
>
|
||||
<SearchToolbarContent>
|
||||
<ToolbarToggleGroup toggleIcon={<SearchIcon />} breakpoint="lg">
|
||||
<ToolbarItem variant="search-filter">
|
||||
{isJobRunning(job.status) ? (
|
||||
<Tooltip
|
||||
content={i18n._(
|
||||
t`Search is disabled while the job is running`
|
||||
)}
|
||||
<Tooltip
|
||||
content={i18n._(t`Job output documentation`)}
|
||||
position="bottom"
|
||||
>
|
||||
<Button
|
||||
component="a"
|
||||
variant="plain"
|
||||
target="_blank"
|
||||
href={`${getDocsBaseUrl(
|
||||
config
|
||||
)}/html/userguide/jobs.html#standard-out-pane`}
|
||||
>
|
||||
<QuestionCircleIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ToolbarItem>
|
||||
</ToolbarToggleGroup>
|
||||
</SearchToolbarContent>
|
||||
</SearchToolbar>
|
||||
<PageControls
|
||||
onScrollFirst={handleScrollFirst}
|
||||
onScrollLast={handleScrollLast}
|
||||
onScrollNext={handleScrollNext}
|
||||
onScrollPrevious={handleScrollPrevious}
|
||||
/>
|
||||
<OutputWrapper cssMap={cssMap}>
|
||||
<InfiniteLoader
|
||||
isRowLoaded={isRowLoaded}
|
||||
loadMoreRows={loadMoreRows}
|
||||
rowCount={remoteRowCount}
|
||||
>
|
||||
{({ onRowsRendered, registerChild }) => (
|
||||
<AutoSizer nonce={window.NONCE_ID} onResize={handleResize}>
|
||||
{({ width, height }) => {
|
||||
return (
|
||||
<>
|
||||
{hasContentLoading ? (
|
||||
<div style={{ width }}>
|
||||
<ContentLoading />
|
||||
</div>
|
||||
) : (
|
||||
<List
|
||||
ref={ref => {
|
||||
registerChild(ref);
|
||||
listRef.current = ref;
|
||||
}}
|
||||
deferredMeasurementCache={cache}
|
||||
height={height || 1}
|
||||
onRowsRendered={onRowsRendered}
|
||||
rowCount={remoteRowCount}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={rowRenderer}
|
||||
scrollToAlignment="start"
|
||||
width={width || 1}
|
||||
overscanRowCount={20}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
>
|
||||
{renderSearchComponent(i18n)}
|
||||
</Tooltip>
|
||||
) : (
|
||||
renderSearchComponent(i18n)
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
<OutputFooter />
|
||||
</OutputWrapper>
|
||||
</CardBody>
|
||||
{showCancelModal && isJobRunning(job.status) && (
|
||||
<AlertModal
|
||||
isOpen={showCancelModal}
|
||||
</ToolbarItem>
|
||||
</ToolbarToggleGroup>
|
||||
</SearchToolbarContent>
|
||||
</SearchToolbar>
|
||||
<PageControls
|
||||
onScrollFirst={handleScrollFirst}
|
||||
onScrollLast={handleScrollLast}
|
||||
onScrollNext={handleScrollNext}
|
||||
onScrollPrevious={handleScrollPrevious}
|
||||
/>
|
||||
<OutputWrapper cssMap={cssMap}>
|
||||
<InfiniteLoader
|
||||
isRowLoaded={isRowLoaded}
|
||||
loadMoreRows={loadMoreRows}
|
||||
rowCount={remoteRowCount}
|
||||
>
|
||||
{({ onRowsRendered, registerChild }) => (
|
||||
<AutoSizer nonce={window.NONCE_ID} onResize={handleResize}>
|
||||
{({ width, height }) => {
|
||||
return (
|
||||
<>
|
||||
{hasContentLoading ? (
|
||||
<div style={{ width }}>
|
||||
<ContentLoading />
|
||||
</div>
|
||||
) : (
|
||||
<List
|
||||
ref={ref => {
|
||||
registerChild(ref);
|
||||
listRef.current = ref;
|
||||
}}
|
||||
deferredMeasurementCache={cache}
|
||||
height={height || 1}
|
||||
onRowsRendered={onRowsRendered}
|
||||
rowCount={remoteRowCount}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={rowRenderer}
|
||||
scrollToAlignment="start"
|
||||
width={width || 1}
|
||||
overscanRowCount={20}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
<OutputFooter />
|
||||
</OutputWrapper>
|
||||
</CardBody>
|
||||
{showCancelModal && isJobRunning(job.status) && (
|
||||
<AlertModal
|
||||
isOpen={showCancelModal}
|
||||
variant="danger"
|
||||
onClose={() => setShowCancelModal(false)}
|
||||
title={i18n._(t`Cancel Job`)}
|
||||
label={i18n._(t`Cancel Job`)}
|
||||
actions={[
|
||||
<Button
|
||||
id="cancel-job-confirm-button"
|
||||
key="delete"
|
||||
variant="danger"
|
||||
onClose={() => setShowCancelModal(false)}
|
||||
title={i18n._(t`Cancel Job`)}
|
||||
label={i18n._(t`Cancel Job`)}
|
||||
actions={[
|
||||
<Button
|
||||
id="cancel-job-confirm-button"
|
||||
key="delete"
|
||||
variant="danger"
|
||||
isDisabled={isCancelling}
|
||||
aria-label={i18n._(t`Cancel job`)}
|
||||
onClick={cancelJob}
|
||||
>
|
||||
{i18n._(t`Cancel job`)}
|
||||
</Button>,
|
||||
<Button
|
||||
id="cancel-job-return-button"
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Return`)}
|
||||
onClick={() => setShowCancelModal(false)}
|
||||
>
|
||||
{i18n._(t`Return`)}
|
||||
</Button>,
|
||||
]}
|
||||
isDisabled={isCancelling}
|
||||
aria-label={i18n._(t`Cancel job`)}
|
||||
onClick={cancelJob}
|
||||
>
|
||||
{i18n._(
|
||||
t`Are you sure you want to submit the request to cancel this job?`
|
||||
)}
|
||||
</AlertModal>
|
||||
)}
|
||||
{dismissableDeleteError && (
|
||||
<AlertModal
|
||||
isOpen={dismissableDeleteError}
|
||||
variant="danger"
|
||||
onClose={dismissDeleteError}
|
||||
title={i18n._(t`Job Delete Error`)}
|
||||
label={i18n._(t`Job Delete Error`)}
|
||||
{i18n._(t`Cancel job`)}
|
||||
</Button>,
|
||||
<Button
|
||||
id="cancel-job-return-button"
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Return`)}
|
||||
onClick={() => setShowCancelModal(false)}
|
||||
>
|
||||
<ErrorDetail error={dismissableDeleteError} />
|
||||
</AlertModal>
|
||||
{i18n._(t`Return`)}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{i18n._(
|
||||
t`Are you sure you want to submit the request to cancel this job?`
|
||||
)}
|
||||
{dismissableCancelError && (
|
||||
<AlertModal
|
||||
isOpen={dismissableCancelError}
|
||||
variant="danger"
|
||||
onClose={dismissCancelError}
|
||||
title={i18n._(t`Job Cancel Error`)}
|
||||
label={i18n._(t`Job Cancel Error`)}
|
||||
>
|
||||
<ErrorDetail error={dismissableCancelError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
</AlertModal>
|
||||
)}
|
||||
</I18n>
|
||||
{dismissableDeleteError && (
|
||||
<AlertModal
|
||||
isOpen={dismissableDeleteError}
|
||||
variant="danger"
|
||||
onClose={dismissDeleteError}
|
||||
title={i18n._(t`Job Delete Error`)}
|
||||
label={i18n._(t`Job Delete Error`)}
|
||||
>
|
||||
<ErrorDetail error={dismissableDeleteError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{dismissableCancelError && (
|
||||
<AlertModal
|
||||
isOpen={dismissableCancelError}
|
||||
variant="danger"
|
||||
onClose={dismissCancelError}
|
||||
title={i18n._(t`Job Cancel Error`)}
|
||||
label={i18n._(t`Job Cancel Error`)}
|
||||
>
|
||||
<ErrorDetail error={dismissableCancelError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -73,8 +73,7 @@ function OrganizationDetail({ i18n, organization }) {
|
||||
const { error, dismissError } = useDismissableError(deleteError);
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.organization(
|
||||
organization,
|
||||
i18n
|
||||
organization
|
||||
);
|
||||
|
||||
if (hasContentLoading) {
|
||||
|
||||
@ -118,8 +118,7 @@ function OrganizationsList({ i18n }) {
|
||||
}
|
||||
};
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.organization(
|
||||
selected[0],
|
||||
i18n
|
||||
selected[0]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -2,14 +2,21 @@ import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
|
||||
import { en } from 'make-plural/plurals';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
|
||||
import english from '../../../locales/en/messages';
|
||||
import OrganizationListItem from './OrganizationListItem';
|
||||
|
||||
i18n.loadLocaleData({ en: { plurals: en } });
|
||||
i18n.load({ en: english });
|
||||
i18n.activate('en');
|
||||
|
||||
describe('<OrganizationListItem />', () => {
|
||||
test('initially renders successfully', () => {
|
||||
mountWithContexts(
|
||||
<I18nProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
|
||||
<table>
|
||||
<tbody>
|
||||
@ -40,7 +47,7 @@ describe('<OrganizationListItem />', () => {
|
||||
|
||||
test('edit button shown to users with edit capabilities', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<I18nProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
|
||||
<table>
|
||||
<tbody>
|
||||
@ -72,7 +79,7 @@ describe('<OrganizationListItem />', () => {
|
||||
|
||||
test('edit button hidden from users without edit capabilities', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<I18nProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
|
||||
<table>
|
||||
<tbody>
|
||||
|
||||
@ -53,10 +53,7 @@ function ProjectDetail({ project, i18n }) {
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(deleteError);
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.project(
|
||||
project,
|
||||
i18n
|
||||
);
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.project(project);
|
||||
let optionsList = '';
|
||||
if (
|
||||
scm_clean ||
|
||||
|
||||
@ -118,8 +118,7 @@ function ProjectList({ i18n }) {
|
||||
};
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.project(
|
||||
selected[0],
|
||||
i18n
|
||||
selected[0]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { bool, oneOf, shape, string } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { shape, string } from 'prop-types';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import {
|
||||
@ -35,20 +35,20 @@ const FormGroup = styled(PFFormGroup)`
|
||||
}
|
||||
`;
|
||||
|
||||
const SettingGroup = withI18n()(
|
||||
({
|
||||
i18n,
|
||||
children,
|
||||
defaultValue,
|
||||
fieldId,
|
||||
helperTextInvalid,
|
||||
isDisabled,
|
||||
isRequired,
|
||||
label,
|
||||
onRevertCallback,
|
||||
popoverContent,
|
||||
validated,
|
||||
}) => (
|
||||
const SettingGroup = ({
|
||||
children,
|
||||
defaultValue,
|
||||
fieldId,
|
||||
helperTextInvalid,
|
||||
isDisabled,
|
||||
isRequired,
|
||||
label,
|
||||
onRevertCallback,
|
||||
popoverContent,
|
||||
validated,
|
||||
}) => {
|
||||
const { i18n } = useLingui();
|
||||
return (
|
||||
<FormGroup
|
||||
fieldId={fieldId}
|
||||
helperTextInvalid={helperTextInvalid}
|
||||
@ -73,43 +73,42 @@ const SettingGroup = withI18n()(
|
||||
>
|
||||
{children}
|
||||
</FormGroup>
|
||||
)
|
||||
);
|
||||
);
|
||||
};
|
||||
const BooleanField = ({ ariaLabel = '', name, config, disabled = false }) => {
|
||||
const [field, meta, helpers] = useField(name);
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const BooleanField = withI18n()(
|
||||
({ i18n, ariaLabel = '', name, config, disabled = false }) => {
|
||||
const [field, meta, helpers] = useField(name);
|
||||
return config ? (
|
||||
<SettingGroup
|
||||
defaultValue={config.default ?? false}
|
||||
fieldId={name}
|
||||
helperTextInvalid={meta.error}
|
||||
return config ? (
|
||||
<SettingGroup
|
||||
defaultValue={config.default ?? false}
|
||||
fieldId={name}
|
||||
helperTextInvalid={meta.error}
|
||||
isDisabled={disabled}
|
||||
label={config.label}
|
||||
popoverContent={config.help_text}
|
||||
>
|
||||
<Switch
|
||||
id={name}
|
||||
ouiaId={name}
|
||||
isChecked={field.value}
|
||||
isDisabled={disabled}
|
||||
label={config.label}
|
||||
popoverContent={config.help_text}
|
||||
>
|
||||
<Switch
|
||||
id={name}
|
||||
ouiaId={name}
|
||||
isChecked={field.value}
|
||||
isDisabled={disabled}
|
||||
label={i18n._(t`On`)}
|
||||
labelOff={i18n._(t`Off`)}
|
||||
onChange={checked => helpers.setValue(checked)}
|
||||
aria-label={ariaLabel || config.label}
|
||||
/>
|
||||
</SettingGroup>
|
||||
) : null;
|
||||
}
|
||||
);
|
||||
label={i18n._(t`On`)}
|
||||
labelOff={i18n._(t`Off`)}
|
||||
onChange={checked => helpers.setValue(checked)}
|
||||
aria-label={ariaLabel || config.label}
|
||||
/>
|
||||
</SettingGroup>
|
||||
) : null;
|
||||
};
|
||||
BooleanField.propTypes = {
|
||||
name: string.isRequired,
|
||||
config: shape({}).isRequired,
|
||||
ariaLabel: string,
|
||||
disabled: bool,
|
||||
};
|
||||
|
||||
const ChoiceField = withI18n()(({ i18n, name, config, isRequired = false }) => {
|
||||
const ChoiceField = ({ name, config, isRequired = false }) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const validate = isRequired ? required(null, i18n) : null;
|
||||
const [field, meta] = useField({ name, validate });
|
||||
const isValid = !meta.error || !meta.touched;
|
||||
@ -137,133 +136,130 @@ const ChoiceField = withI18n()(({ i18n, name, config, isRequired = false }) => {
|
||||
/>
|
||||
</SettingGroup>
|
||||
) : null;
|
||||
});
|
||||
};
|
||||
ChoiceField.propTypes = {
|
||||
name: string.isRequired,
|
||||
config: shape({}).isRequired,
|
||||
isRequired: bool,
|
||||
};
|
||||
|
||||
const EncryptedField = withI18n()(
|
||||
({ i18n, name, config, isRequired = false }) => {
|
||||
const validate = isRequired ? required(null, i18n) : null;
|
||||
const [, meta] = useField({ name, validate });
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
const EncryptedField = ({ name, config, isRequired = false }) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
return config ? (
|
||||
<SettingGroup
|
||||
defaultValue={config.default ?? ''}
|
||||
fieldId={name}
|
||||
helperTextInvalid={meta.error}
|
||||
isRequired={isRequired}
|
||||
label={config.label}
|
||||
popoverContent={config.help_text}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
>
|
||||
<InputGroup>
|
||||
<PasswordInput
|
||||
id={name}
|
||||
name={name}
|
||||
label={config.label}
|
||||
validate={validate}
|
||||
isRequired={isRequired}
|
||||
/>
|
||||
</InputGroup>
|
||||
</SettingGroup>
|
||||
) : null;
|
||||
}
|
||||
);
|
||||
const validate = isRequired ? required(null, i18n) : null;
|
||||
const [, meta] = useField({ name, validate });
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
|
||||
return config ? (
|
||||
<SettingGroup
|
||||
defaultValue={config.default ?? ''}
|
||||
fieldId={name}
|
||||
helperTextInvalid={meta.error}
|
||||
isRequired={isRequired}
|
||||
label={config.label}
|
||||
popoverContent={config.help_text}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
>
|
||||
<InputGroup>
|
||||
<PasswordInput
|
||||
id={name}
|
||||
name={name}
|
||||
label={config.label}
|
||||
validate={validate}
|
||||
isRequired={isRequired}
|
||||
/>
|
||||
</InputGroup>
|
||||
</SettingGroup>
|
||||
) : null;
|
||||
};
|
||||
EncryptedField.propTypes = {
|
||||
name: string.isRequired,
|
||||
config: shape({}).isRequired,
|
||||
isRequired: bool,
|
||||
};
|
||||
|
||||
const InputField = withI18n()(
|
||||
({ i18n, name, config, type = 'text', isRequired = false }) => {
|
||||
const min_value = config?.min_value ?? Number.MIN_SAFE_INTEGER;
|
||||
const max_value = config?.max_value ?? Number.MAX_SAFE_INTEGER;
|
||||
const validators = [
|
||||
...(isRequired ? [required(null, i18n)] : []),
|
||||
...(type === 'url' ? [url(i18n)] : []),
|
||||
...(type === 'number'
|
||||
? [integer(i18n), minMaxValue(min_value, max_value, i18n)]
|
||||
: []),
|
||||
];
|
||||
const [field, meta] = useField({ name, validate: combine(validators) });
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
const InputField = ({ name, config, type = 'text', isRequired = false }) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
return config ? (
|
||||
<SettingGroup
|
||||
defaultValue={config.default ?? ''}
|
||||
fieldId={name}
|
||||
helperTextInvalid={meta.error}
|
||||
const min_value = config?.min_value ?? Number.MIN_SAFE_INTEGER;
|
||||
const max_value = config?.max_value ?? Number.MAX_SAFE_INTEGER;
|
||||
const validators = [
|
||||
...(isRequired ? [required(null, i18n)] : []),
|
||||
...(type === 'url' ? [url(i18n)] : []),
|
||||
...(type === 'number'
|
||||
? [integer(i18n), minMaxValue(min_value, max_value, i18n)]
|
||||
: []),
|
||||
];
|
||||
const [field, meta] = useField({ name, validate: combine(validators) });
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
|
||||
return config ? (
|
||||
<SettingGroup
|
||||
defaultValue={config.default ?? ''}
|
||||
fieldId={name}
|
||||
helperTextInvalid={meta.error}
|
||||
isRequired={isRequired}
|
||||
label={config.label}
|
||||
popoverContent={config.help_text}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
>
|
||||
<TextInput
|
||||
id={name}
|
||||
isRequired={isRequired}
|
||||
label={config.label}
|
||||
popoverContent={config.help_text}
|
||||
placeholder={config.placeholder}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
>
|
||||
<TextInput
|
||||
id={name}
|
||||
isRequired={isRequired}
|
||||
placeholder={config.placeholder}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(value, event) => {
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
</SettingGroup>
|
||||
) : null;
|
||||
}
|
||||
);
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(value, event) => {
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
</SettingGroup>
|
||||
) : null;
|
||||
};
|
||||
InputField.propTypes = {
|
||||
name: string.isRequired,
|
||||
config: shape({}).isRequired,
|
||||
type: oneOf(['text', 'number', 'url']),
|
||||
isRequired: bool,
|
||||
};
|
||||
|
||||
const TextAreaField = withI18n()(
|
||||
({ i18n, name, config, isRequired = false }) => {
|
||||
const validate = isRequired ? required(null, i18n) : null;
|
||||
const [field, meta] = useField({ name, validate });
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
const TextAreaField = ({ name, config, isRequired = false }) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
return config ? (
|
||||
<SettingGroup
|
||||
defaultValue={config.default || ''}
|
||||
fieldId={name}
|
||||
helperTextInvalid={meta.error}
|
||||
const validate = isRequired ? required(null, i18n) : null;
|
||||
const [field, meta] = useField({ name, validate });
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
|
||||
return config ? (
|
||||
<SettingGroup
|
||||
defaultValue={config.default || ''}
|
||||
fieldId={name}
|
||||
helperTextInvalid={meta.error}
|
||||
isRequired={isRequired}
|
||||
label={config.label}
|
||||
popoverContent={config.help_text}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
>
|
||||
<TextArea
|
||||
id={name}
|
||||
isRequired={isRequired}
|
||||
label={config.label}
|
||||
popoverContent={config.help_text}
|
||||
placeholder={config.placeholder}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
>
|
||||
<TextArea
|
||||
id={name}
|
||||
isRequired={isRequired}
|
||||
placeholder={config.placeholder}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(value, event) => {
|
||||
field.onChange(event);
|
||||
}}
|
||||
resizeOrientation="vertical"
|
||||
/>
|
||||
</SettingGroup>
|
||||
) : null;
|
||||
}
|
||||
);
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
onChange={(value, event) => {
|
||||
field.onChange(event);
|
||||
}}
|
||||
resizeOrientation="vertical"
|
||||
/>
|
||||
</SettingGroup>
|
||||
) : null;
|
||||
};
|
||||
TextAreaField.propTypes = {
|
||||
name: string.isRequired,
|
||||
config: shape({}).isRequired,
|
||||
isRequired: bool,
|
||||
};
|
||||
|
||||
const ObjectField = withI18n()(({ i18n, name, config, isRequired = false }) => {
|
||||
const ObjectField = ({ name, config, isRequired = false }) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const validate = isRequired ? required(null, i18n) : null;
|
||||
const [field, meta, helpers] = useField({ name, validate });
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
@ -297,76 +293,79 @@ const ObjectField = withI18n()(({ i18n, name, config, isRequired = false }) => {
|
||||
</SettingGroup>
|
||||
</FormFullWidthLayout>
|
||||
) : null;
|
||||
});
|
||||
};
|
||||
ObjectField.propTypes = {
|
||||
name: string.isRequired,
|
||||
config: shape({}).isRequired,
|
||||
isRequired: bool,
|
||||
};
|
||||
|
||||
const FileUploadIconWrapper = styled.div`
|
||||
margin: var(--pf-global--spacer--md);
|
||||
`;
|
||||
const FileUploadField = withI18n()(
|
||||
({ i18n, name, config, type = 'text', isRequired = false }) => {
|
||||
const validate = isRequired ? required(null, i18n) : null;
|
||||
const [filename, setFilename] = useState('');
|
||||
const [fileIsUploading, setFileIsUploading] = useState(false);
|
||||
const [field, meta, helpers] = useField({ name, validate });
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
const FileUploadField = ({
|
||||
name,
|
||||
config,
|
||||
type = 'text',
|
||||
isRequired = false,
|
||||
}) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
return config ? (
|
||||
<FormFullWidthLayout>
|
||||
<SettingGroup
|
||||
defaultValue={config.default ?? ''}
|
||||
fieldId={name}
|
||||
helperTextInvalid={meta.error}
|
||||
isRequired={isRequired}
|
||||
label={config.label}
|
||||
popoverContent={config.help_text}
|
||||
const validate = isRequired ? required(null, i18n) : null;
|
||||
const [filename, setFilename] = useState('');
|
||||
const [fileIsUploading, setFileIsUploading] = useState(false);
|
||||
const [field, meta, helpers] = useField({ name, validate });
|
||||
const isValid = !(meta.touched && meta.error);
|
||||
|
||||
return config ? (
|
||||
<FormFullWidthLayout>
|
||||
<SettingGroup
|
||||
defaultValue={config.default ?? ''}
|
||||
fieldId={name}
|
||||
helperTextInvalid={meta.error}
|
||||
isRequired={isRequired}
|
||||
label={config.label}
|
||||
popoverContent={config.help_text}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
onRevertCallback={() => setFilename('')}
|
||||
>
|
||||
<FileUpload
|
||||
{...field}
|
||||
id={name}
|
||||
type={type}
|
||||
filename={filename}
|
||||
onChange={(value, title) => {
|
||||
helpers.setValue(value);
|
||||
setFilename(title);
|
||||
}}
|
||||
onReadStarted={() => setFileIsUploading(true)}
|
||||
onReadFinished={() => setFileIsUploading(false)}
|
||||
isLoading={fileIsUploading}
|
||||
allowEditingUploadedText
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
onRevertCallback={() => setFilename('')}
|
||||
hideDefaultPreview={type === 'dataURL'}
|
||||
>
|
||||
<FileUpload
|
||||
{...field}
|
||||
id={name}
|
||||
type={type}
|
||||
filename={filename}
|
||||
onChange={(value, title) => {
|
||||
helpers.setValue(value);
|
||||
setFilename(title);
|
||||
}}
|
||||
onReadStarted={() => setFileIsUploading(true)}
|
||||
onReadFinished={() => setFileIsUploading(false)}
|
||||
isLoading={fileIsUploading}
|
||||
allowEditingUploadedText
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
hideDefaultPreview={type === 'dataURL'}
|
||||
>
|
||||
{type === 'dataURL' && (
|
||||
<FileUploadIconWrapper>
|
||||
{field.value ? (
|
||||
<img
|
||||
src={field.value}
|
||||
alt={filename}
|
||||
height="200px"
|
||||
width="200px"
|
||||
/>
|
||||
) : (
|
||||
<FileUploadIcon size="lg" />
|
||||
)}
|
||||
</FileUploadIconWrapper>
|
||||
)}
|
||||
</FileUpload>
|
||||
</SettingGroup>
|
||||
</FormFullWidthLayout>
|
||||
) : null;
|
||||
}
|
||||
);
|
||||
{type === 'dataURL' && (
|
||||
<FileUploadIconWrapper>
|
||||
{field.value ? (
|
||||
<img
|
||||
src={field.value}
|
||||
alt={filename}
|
||||
height="200px"
|
||||
width="200px"
|
||||
/>
|
||||
) : (
|
||||
<FileUploadIcon size="lg" />
|
||||
)}
|
||||
</FileUploadIconWrapper>
|
||||
)}
|
||||
</FileUpload>
|
||||
</SettingGroup>
|
||||
</FormFullWidthLayout>
|
||||
) : null;
|
||||
};
|
||||
FileUploadField.propTypes = {
|
||||
name: string.isRequired,
|
||||
config: shape({}).isRequired,
|
||||
isRequired: bool,
|
||||
};
|
||||
|
||||
export {
|
||||
|
||||
@ -3,6 +3,7 @@ import { mount } from 'enzyme';
|
||||
import { Formik } from 'formik';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import {
|
||||
BooleanField,
|
||||
@ -13,13 +14,17 @@ import {
|
||||
ObjectField,
|
||||
TextAreaField,
|
||||
} from './SharedFields';
|
||||
import en from '../../../locales/en/messages';
|
||||
|
||||
describe('Setting form fields', () => {
|
||||
test('BooleanField renders the expected content', async () => {
|
||||
const outerNode = document.createElement('div');
|
||||
document.body.appendChild(outerNode);
|
||||
i18n.loadLocaleData({ en: { plurals: en } });
|
||||
i18n.load({ en });
|
||||
i18n.activate('en');
|
||||
const wrapper = mount(
|
||||
<I18nProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<Formik
|
||||
initialValues={{
|
||||
boolean: true,
|
||||
@ -244,7 +249,10 @@ describe('Setting form fields', () => {
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
expect(wrapper.find('FileUploadField')).toHaveLength(1);
|
||||
|
||||
expect(
|
||||
wrapper.find('FileUploadField[value="mock file value"]')
|
||||
).toHaveLength(1);
|
||||
expect(wrapper.find('label').text()).toEqual('mock file label');
|
||||
expect(wrapper.find('input#mock_file-filename').prop('value')).toEqual('');
|
||||
await act(async () => {
|
||||
|
||||
@ -2,14 +2,21 @@ import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { en } from 'make-plural/plurals';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
|
||||
import TeamListItem from './TeamListItem';
|
||||
import english from '../../../locales/en/messages';
|
||||
|
||||
i18n.loadLocaleData({ en: { plurals: en } });
|
||||
i18n.load({ en: english });
|
||||
i18n.activate('en');
|
||||
|
||||
describe('<TeamListItem />', () => {
|
||||
test('initially renders succesfully', () => {
|
||||
mountWithContexts(
|
||||
<I18nProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
|
||||
<table>
|
||||
<tbody>
|
||||
@ -35,7 +42,7 @@ describe('<TeamListItem />', () => {
|
||||
});
|
||||
test('edit button shown to users with edit capabilities', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<I18nProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
|
||||
<table>
|
||||
<tbody>
|
||||
@ -62,7 +69,7 @@ describe('<TeamListItem />', () => {
|
||||
});
|
||||
test('edit button hidden from users without edit capabilities', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<I18nProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
|
||||
<table>
|
||||
<tbody>
|
||||
|
||||
@ -98,8 +98,7 @@ function JobTemplateDetail({ i18n, template }) {
|
||||
const { error, dismissError } = useDismissableError(deleteError);
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
|
||||
template,
|
||||
i18n
|
||||
template
|
||||
);
|
||||
const canLaunch =
|
||||
summary_fields.user_capabilities && summary_fields.user_capabilities.start;
|
||||
|
||||
@ -104,8 +104,7 @@ function WorkflowJobTemplateDetail({ template, i18n }) {
|
||||
}));
|
||||
|
||||
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
|
||||
template,
|
||||
i18n
|
||||
template
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -2,10 +2,17 @@ import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { en } from 'make-plural/plurals';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
|
||||
import mockDetails from '../data.user.json';
|
||||
import UserListItem from './UserListItem';
|
||||
import english from '../../../locales/en/messages';
|
||||
|
||||
i18n.loadLocaleData({ en: { plurals: en } });
|
||||
i18n.load({ en: english });
|
||||
i18n.activate('en');
|
||||
|
||||
let wrapper;
|
||||
|
||||
@ -16,7 +23,7 @@ afterEach(() => {
|
||||
describe('UserListItem with full permissions', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<I18nProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<MemoryRouter initialEntries={['/users']} initialIndex={0}>
|
||||
<table>
|
||||
<tbody>
|
||||
@ -52,7 +59,7 @@ describe('UserListItem with full permissions', () => {
|
||||
describe('UserListItem without full permissions', () => {
|
||||
test('edit button hidden from users without edit capabilities', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<I18nProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<MemoryRouter initialEntries={['/users']} initialIndex={0}>
|
||||
<table>
|
||||
<tbody>
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { en } from 'make-plural/plurals';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import UserTeamListItem from './UserTeamListItem';
|
||||
import english from '../../../locales/en/messages';
|
||||
|
||||
i18n.loadLocaleData({ en: { plurals: en } });
|
||||
i18n.load({ en: english });
|
||||
i18n.activate('en');
|
||||
|
||||
describe('<UserTeamListItem />', () => {
|
||||
test('should render item', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<I18nProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<MemoryRouter initialEntries={['/teams']} initialIndex={0}>
|
||||
<UserTeamListItem
|
||||
team={{
|
||||
|
||||
@ -10,6 +10,8 @@ import {
|
||||
WorkflowJobTemplatesAPI,
|
||||
WorkflowJobTemplateNodesAPI,
|
||||
CredentialsAPI,
|
||||
ExecutionEnvironmentsAPI,
|
||||
CredentialInputSourcesAPI,
|
||||
} from '../api';
|
||||
|
||||
jest.mock('../api/models/Credentials');
|
||||
@ -19,17 +21,8 @@ jest.mock('../api/models/JobTemplates');
|
||||
jest.mock('../api/models/Projects');
|
||||
jest.mock('../api/models/WorkflowJobTemplates');
|
||||
jest.mock('../api/models/WorkflowJobTemplateNodes');
|
||||
|
||||
const i18n = {
|
||||
_: key => {
|
||||
if (key.values) {
|
||||
Object.entries(key.values).forEach(([k, v]) => {
|
||||
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
|
||||
});
|
||||
}
|
||||
return key.id;
|
||||
},
|
||||
};
|
||||
jest.mock('../api/models/CredentialInputSources');
|
||||
jest.mock('../api/models/ExecutionEnvironments');
|
||||
|
||||
describe('delete details', () => {
|
||||
afterEach(() => {
|
||||
@ -38,7 +31,7 @@ describe('delete details', () => {
|
||||
|
||||
test('should call api for credentials list', () => {
|
||||
getRelatedResourceDeleteCounts(
|
||||
relatedResourceDeleteRequests.credential({ id: 1 }, i18n)
|
||||
relatedResourceDeleteRequests.credential({ id: 1 })
|
||||
);
|
||||
expect(InventoriesAPI.read).toBeCalledWith({
|
||||
insights_credential: 1,
|
||||
@ -52,7 +45,7 @@ describe('delete details', () => {
|
||||
|
||||
test('should call api for projects list', () => {
|
||||
getRelatedResourceDeleteCounts(
|
||||
relatedResourceDeleteRequests.project({ id: 1 }, i18n)
|
||||
relatedResourceDeleteRequests.project({ id: 1 })
|
||||
);
|
||||
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
|
||||
unified_job_template: 1,
|
||||
@ -65,7 +58,7 @@ describe('delete details', () => {
|
||||
|
||||
test('should call api for templates list', () => {
|
||||
getRelatedResourceDeleteCounts(
|
||||
relatedResourceDeleteRequests.template({ id: 1 }, i18n)
|
||||
relatedResourceDeleteRequests.template({ id: 1 })
|
||||
);
|
||||
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
|
||||
unified_job_template: 1,
|
||||
@ -74,7 +67,7 @@ describe('delete details', () => {
|
||||
|
||||
test('should call api for credential type list', () => {
|
||||
getRelatedResourceDeleteCounts(
|
||||
relatedResourceDeleteRequests.credentialType({ id: 1 }, i18n)
|
||||
relatedResourceDeleteRequests.credentialType({ id: 1 })
|
||||
);
|
||||
expect(CredentialsAPI.read).toBeCalledWith({
|
||||
credential_type__id: 1,
|
||||
@ -83,7 +76,7 @@ describe('delete details', () => {
|
||||
|
||||
test('should call api for inventory list', () => {
|
||||
getRelatedResourceDeleteCounts(
|
||||
relatedResourceDeleteRequests.inventory({ id: 1 }, i18n)
|
||||
relatedResourceDeleteRequests.inventory({ id: 1 })
|
||||
);
|
||||
expect(JobTemplatesAPI.read).toBeCalledWith({ inventory: 1 });
|
||||
expect(WorkflowJobTemplatesAPI.read).toBeCalledWith({
|
||||
@ -96,7 +89,7 @@ describe('delete details', () => {
|
||||
data: [{ inventory_source: 2 }],
|
||||
});
|
||||
await getRelatedResourceDeleteCounts(
|
||||
relatedResourceDeleteRequests.inventorySource(1, i18n)
|
||||
relatedResourceDeleteRequests.inventorySource(1)
|
||||
);
|
||||
expect(InventoriesAPI.updateSources).toBeCalledWith(1);
|
||||
expect(WorkflowJobTemplateNodesAPI.read).toBeCalledWith({
|
||||
@ -106,7 +99,7 @@ describe('delete details', () => {
|
||||
|
||||
test('should call api for organization list', async () => {
|
||||
getRelatedResourceDeleteCounts(
|
||||
relatedResourceDeleteRequests.organization({ id: 1 }, i18n)
|
||||
relatedResourceDeleteRequests.organization({ id: 1 })
|
||||
);
|
||||
expect(CredentialsAPI.read).toBeCalledWith({ organization: 1 });
|
||||
});
|
||||
@ -123,7 +116,7 @@ describe('delete details', () => {
|
||||
},
|
||||
});
|
||||
const { error } = await getRelatedResourceDeleteCounts(
|
||||
relatedResourceDeleteRequests.inventorySource(1, i18n)
|
||||
relatedResourceDeleteRequests.inventorySource(1)
|
||||
);
|
||||
|
||||
expect(InventoriesAPI.updateSources).toBeCalledWith(1);
|
||||
@ -131,14 +124,24 @@ describe('delete details', () => {
|
||||
});
|
||||
|
||||
test('should return proper results', async () => {
|
||||
JobTemplatesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||
JobTemplatesAPI.read.mockResolvedValue({ data: { count: 1 } });
|
||||
|
||||
InventorySourcesAPI.read.mockResolvedValue({ data: { count: 10 } });
|
||||
CredentialInputSourcesAPI.read.mockResolvedValue({ data: { count: 20 } });
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({ data: { count: 30 } });
|
||||
ProjectsAPI.read.mockResolvedValue({ data: { count: 2 } });
|
||||
InventoriesAPI.read.mockResolvedValue({ data: { count: 3 } });
|
||||
InventorySourcesAPI.read.mockResolvedValue({ data: { count: 0 } });
|
||||
|
||||
const { results } = await getRelatedResourceDeleteCounts(
|
||||
relatedResourceDeleteRequests.credential({ id: 1 }, i18n)
|
||||
relatedResourceDeleteRequests.credential({ id: 1 })
|
||||
);
|
||||
expect(results).toEqual({ Projects: 2, Inventories: 3 });
|
||||
expect(results).toEqual({
|
||||
'Job Templates': 1,
|
||||
Projects: 2,
|
||||
Inventories: 3,
|
||||
'Inventory Sources': 10,
|
||||
'Credential Input Sources': 20,
|
||||
'Execution Environments': 30,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { i18n } from '@lingui/core';
|
||||
|
||||
import {
|
||||
UnifiedJobTemplatesAPI,
|
||||
@ -46,7 +47,7 @@ export async function getRelatedResourceDeleteCounts(requests) {
|
||||
}
|
||||
|
||||
export const relatedResourceDeleteRequests = {
|
||||
credential: (selected, i18n) => [
|
||||
credential: selected => [
|
||||
{
|
||||
request: () =>
|
||||
JobTemplatesAPI.read({
|
||||
@ -77,7 +78,7 @@ export const relatedResourceDeleteRequests = {
|
||||
CredentialInputSourcesAPI.read({
|
||||
source_credential: selected.id,
|
||||
}),
|
||||
label: i18n._(t`Credential`),
|
||||
label: i18n._(t`Credential Input Sources`),
|
||||
},
|
||||
{
|
||||
request: () =>
|
||||
@ -88,7 +89,7 @@ export const relatedResourceDeleteRequests = {
|
||||
},
|
||||
],
|
||||
|
||||
credentialType: (selected, i18n) => [
|
||||
credentialType: selected => [
|
||||
{
|
||||
request: async () =>
|
||||
CredentialsAPI.read({
|
||||
@ -98,7 +99,7 @@ export const relatedResourceDeleteRequests = {
|
||||
},
|
||||
],
|
||||
|
||||
inventory: (selected, i18n) => [
|
||||
inventory: selected => [
|
||||
{
|
||||
request: async () =>
|
||||
JobTemplatesAPI.read({
|
||||
@ -112,7 +113,7 @@ export const relatedResourceDeleteRequests = {
|
||||
},
|
||||
],
|
||||
|
||||
inventorySource: (inventoryId, i18n, inventorySource) => [
|
||||
inventorySource: (inventoryId, inventorySource) => [
|
||||
{
|
||||
request: async () => {
|
||||
try {
|
||||
@ -147,7 +148,7 @@ export const relatedResourceDeleteRequests = {
|
||||
},
|
||||
],
|
||||
|
||||
project: (selected, i18n) => [
|
||||
project: selected => [
|
||||
{
|
||||
request: () =>
|
||||
JobTemplatesAPI.read({
|
||||
@ -171,7 +172,7 @@ export const relatedResourceDeleteRequests = {
|
||||
},
|
||||
],
|
||||
|
||||
template: (selected, i18n) => [
|
||||
template: selected => [
|
||||
{
|
||||
request: async () =>
|
||||
WorkflowJobTemplateNodesAPI.read({
|
||||
@ -181,7 +182,7 @@ export const relatedResourceDeleteRequests = {
|
||||
},
|
||||
],
|
||||
|
||||
organization: (selected, i18n) => [
|
||||
organization: selected => [
|
||||
{
|
||||
request: async () =>
|
||||
CredentialsAPI.read({
|
||||
@ -232,7 +233,7 @@ export const relatedResourceDeleteRequests = {
|
||||
label: i18n._(t`Applications`),
|
||||
},
|
||||
],
|
||||
executionEnvironment: (selected, i18n) => [
|
||||
executionEnvironment: selected => [
|
||||
{
|
||||
request: async () =>
|
||||
UnifiedJobTemplatesAPI.read({
|
||||
@ -283,7 +284,7 @@ export const relatedResourceDeleteRequests = {
|
||||
label: [i18n._(t`Workflow Job Template Nodes`)],
|
||||
},
|
||||
],
|
||||
instanceGroup: (selected, i18n) => [
|
||||
instanceGroup: selected => [
|
||||
{
|
||||
request: () => OrganizationsAPI.read({ instance_groups: selected.id }),
|
||||
label: i18n._(t`Organizations`),
|
||||
|
||||
@ -20,9 +20,7 @@ describe('validators', () => {
|
||||
});
|
||||
|
||||
test('required returns default message if value missing', () => {
|
||||
expect(required(null, i18n)('')).toEqual({
|
||||
id: 'This field must not be blank',
|
||||
});
|
||||
expect(required(null, i18n)('')).toEqual('This field must not be blank');
|
||||
});
|
||||
|
||||
test('required returns custom message if value missing', () => {
|
||||
@ -30,18 +28,14 @@ describe('validators', () => {
|
||||
});
|
||||
|
||||
test('required interprets white space as empty value', () => {
|
||||
expect(required(null, i18n)(' ')).toEqual({
|
||||
id: 'This field must not be blank',
|
||||
});
|
||||
expect(required(null, i18n)('\t')).toEqual({
|
||||
id: 'This field must not be blank',
|
||||
});
|
||||
expect(required(null, i18n)(' ')).toEqual('This field must not be blank');
|
||||
expect(required(null, i18n)('\t')).toEqual('This field must not be blank');
|
||||
});
|
||||
|
||||
test('required interprets undefined as empty value', () => {
|
||||
expect(required(null, i18n)(undefined)).toEqual({
|
||||
id: 'This field must not be blank',
|
||||
});
|
||||
expect(required(null, i18n)(undefined)).toEqual(
|
||||
'This field must not be blank'
|
||||
);
|
||||
});
|
||||
|
||||
test('required interprets 0 as non-empty value', () => {
|
||||
@ -57,10 +51,9 @@ describe('validators', () => {
|
||||
});
|
||||
|
||||
test('maxLength rejects value above max', () => {
|
||||
expect(maxLength(8, i18n)('abracadbra')).toEqual({
|
||||
id: 'This field must not exceed {max} characters',
|
||||
values: { max: 8 },
|
||||
});
|
||||
expect(maxLength(8, i18n)('abracadbra')).toEqual(
|
||||
'This field must not exceed {max} characters'
|
||||
);
|
||||
});
|
||||
|
||||
test('minLength accepts value above min', () => {
|
||||
@ -72,22 +65,21 @@ describe('validators', () => {
|
||||
});
|
||||
|
||||
test('minLength rejects value below min', () => {
|
||||
expect(minLength(12, i18n)('abracadbra')).toEqual({
|
||||
id: 'This field must be at least {min} characters',
|
||||
values: { min: 12 },
|
||||
});
|
||||
expect(minLength(12, i18n)('abracadbra')).toEqual(
|
||||
'This field must be at least {min} characters'
|
||||
);
|
||||
});
|
||||
|
||||
test('noWhiteSpace returns error', () => {
|
||||
expect(noWhiteSpace(i18n)('this has spaces')).toEqual({
|
||||
id: 'This field must not contain spaces',
|
||||
});
|
||||
expect(noWhiteSpace(i18n)('this has\twhitespace')).toEqual({
|
||||
id: 'This field must not contain spaces',
|
||||
});
|
||||
expect(noWhiteSpace(i18n)('this\nhas\nnewlines')).toEqual({
|
||||
id: 'This field must not contain spaces',
|
||||
});
|
||||
expect(noWhiteSpace(i18n)('this has spaces')).toEqual(
|
||||
'This field must not contain spaces'
|
||||
);
|
||||
expect(noWhiteSpace(i18n)('this has\twhitespace')).toEqual(
|
||||
'This field must not contain spaces'
|
||||
);
|
||||
expect(noWhiteSpace(i18n)('this\nhas\nnewlines')).toEqual(
|
||||
'This field must not contain spaces'
|
||||
);
|
||||
});
|
||||
|
||||
test('noWhiteSpace should accept valid string', () => {
|
||||
@ -103,15 +95,11 @@ describe('validators', () => {
|
||||
});
|
||||
|
||||
test('integer should reject decimal/float', () => {
|
||||
expect(integer(i18n)(13.1)).toEqual({
|
||||
id: 'This field must be an integer',
|
||||
});
|
||||
expect(integer(i18n)(13.1)).toEqual('This field must be an integer');
|
||||
});
|
||||
|
||||
test('integer should reject string containing alphanum', () => {
|
||||
expect(integer(i18n)('15a')).toEqual({
|
||||
id: 'This field must be an integer',
|
||||
});
|
||||
expect(integer(i18n)('15a')).toEqual('This field must be an integer');
|
||||
});
|
||||
|
||||
test('number should accept number (number)', () => {
|
||||
@ -136,15 +124,11 @@ describe('validators', () => {
|
||||
});
|
||||
|
||||
test('number should reject string containing alphanum', () => {
|
||||
expect(number(i18n)('15a')).toEqual({
|
||||
id: 'This field must be a number',
|
||||
});
|
||||
expect(number(i18n)('15a')).toEqual('This field must be a number');
|
||||
});
|
||||
|
||||
test('url should reject incomplete url', () => {
|
||||
expect(url(i18n)('abcd')).toEqual({
|
||||
id: 'Please enter a valid URL',
|
||||
});
|
||||
expect(url(i18n)('abcd')).toEqual('Please enter a valid URL');
|
||||
});
|
||||
|
||||
test('url should accept fully qualified url', () => {
|
||||
@ -156,43 +140,37 @@ describe('validators', () => {
|
||||
});
|
||||
|
||||
test('url should reject short protocol', () => {
|
||||
expect(url(i18n)('h://example.com/foo')).toEqual({
|
||||
id: 'Please enter a valid URL',
|
||||
});
|
||||
expect(url(i18n)('h://example.com/foo')).toEqual(
|
||||
'Please enter a valid URL'
|
||||
);
|
||||
});
|
||||
|
||||
test('combine should run all validators', () => {
|
||||
const validators = [required(null, i18n), noWhiteSpace(i18n)];
|
||||
expect(combine(validators)('')).toEqual({
|
||||
id: 'This field must not be blank',
|
||||
});
|
||||
expect(combine(validators)('one two')).toEqual({
|
||||
id: 'This field must not contain spaces',
|
||||
});
|
||||
expect(combine(validators)('')).toEqual('This field must not be blank');
|
||||
expect(combine(validators)('one two')).toEqual(
|
||||
'This field must not contain spaces'
|
||||
);
|
||||
expect(combine(validators)('ok')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('combine should skip null validators', () => {
|
||||
const validators = [required(null, i18n), null];
|
||||
expect(combine(validators)('')).toEqual({
|
||||
id: 'This field must not be blank',
|
||||
});
|
||||
expect(combine(validators)('')).toEqual('This field must not be blank');
|
||||
expect(combine(validators)('ok')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('regExp rejects invalid regular expression', () => {
|
||||
expect(regExp(i18n)('[')).toEqual({
|
||||
id: 'This field must be a regular expression',
|
||||
});
|
||||
expect(regExp(i18n)('[')).toEqual(
|
||||
'This field must be a regular expression'
|
||||
);
|
||||
expect(regExp(i18n)('')).toBeUndefined();
|
||||
expect(regExp(i18n)('ok')).toBeUndefined();
|
||||
expect(regExp(i18n)('[^a-zA-Z]')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('email validator rejects obviously invalid email ', () => {
|
||||
expect(requiredEmail(i18n)('foobar321')).toEqual({
|
||||
id: 'Invalid email address',
|
||||
});
|
||||
expect(requiredEmail(i18n)('foobar321')).toEqual('Invalid email address');
|
||||
});
|
||||
|
||||
test('bob has email', () => {
|
||||
|
||||
@ -3,41 +3,20 @@
|
||||
* derived from https://lingui.js.org/guides/testing.html
|
||||
*/
|
||||
import React from 'react';
|
||||
import { shape, object, string, arrayOf } from 'prop-types';
|
||||
import { shape, string, arrayOf } from 'prop-types';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { MemoryRouter, Router } from 'react-router-dom';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
import { ConfigProvider } from '../src/contexts/Config'
|
||||
import { i18n } from '@lingui/core';
|
||||
import { en } from 'make-plural/plurals';
|
||||
import english from '../src/locales/en/messages';
|
||||
import { ConfigProvider } from '../src/contexts/Config';
|
||||
|
||||
const language = 'en-US';
|
||||
const intlProvider = new I18nProvider(
|
||||
{
|
||||
language,
|
||||
catalogs: {
|
||||
[language]: {},
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
const {
|
||||
linguiPublisher: { i18n: originalI18n },
|
||||
} = intlProvider.getChildContext();
|
||||
i18n.loadLocaleData({ en: { plurals: en } });
|
||||
i18n.load({ en: english });
|
||||
i18n.activate('en');
|
||||
|
||||
const defaultContexts = {
|
||||
linguiPublisher: {
|
||||
i18n: {
|
||||
...originalI18n,
|
||||
_: key => {
|
||||
if (key.values) {
|
||||
Object.entries(key.values).forEach(([k, v]) => {
|
||||
key.id = key.id.replace(new RegExp(`\\{${k}\\}`), v);
|
||||
});
|
||||
}
|
||||
return key.id;
|
||||
}, // provide _ macro, for just passing down the key
|
||||
toJSON: () => '/i18n/',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
ansible_version: null,
|
||||
custom_virtualenvs: [],
|
||||
@ -45,8 +24,8 @@ const defaultContexts = {
|
||||
me: { is_superuser: true },
|
||||
toJSON: () => '/config/',
|
||||
license_info: {
|
||||
valid_key: true
|
||||
}
|
||||
valid_key: true,
|
||||
},
|
||||
},
|
||||
router: {
|
||||
history_: {
|
||||
@ -89,15 +68,19 @@ function wrapContexts(node, context) {
|
||||
const component = React.cloneElement(children, props);
|
||||
if (router.history) {
|
||||
return (
|
||||
<ConfigProvider value={config}>
|
||||
<Router history={router.history}>{component}</Router>
|
||||
</ConfigProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<ConfigProvider value={config}>
|
||||
<Router history={router.history}>{component}</Router>
|
||||
</ConfigProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ConfigProvider value={config}>
|
||||
<MemoryRouter>{component}</MemoryRouter>
|
||||
</ConfigProvider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
<ConfigProvider value={config}>
|
||||
<MemoryRouter>{component}</MemoryRouter>
|
||||
</ConfigProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -127,9 +110,6 @@ export function shallowWithContexts(node, options = {}) {
|
||||
export function mountWithContexts(node, options = {}) {
|
||||
const context = applyDefaultContexts(options.context);
|
||||
const childContextTypes = {
|
||||
linguiPublisher: shape({
|
||||
i18n: object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
}).isRequired,
|
||||
config: shape({
|
||||
ansible_version: string,
|
||||
custom_virtualenvs: arrayOf(string),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user