mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 18:37:36 -02:30
Add Crendential Type Details
Add credential type Details See: https://github.com/ansible/awx/issues/7430
This commit is contained in:
@@ -1,25 +1,121 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
import {
|
||||||
|
Link,
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
Switch,
|
||||||
|
useLocation,
|
||||||
|
useParams,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
|
import { CredentialTypesAPI } from '../../api';
|
||||||
|
import RoutedTabs from '../../components/RoutedTabs';
|
||||||
|
import ContentError from '../../components/ContentError';
|
||||||
|
import ContentLoading from '../../components/ContentLoading';
|
||||||
|
|
||||||
import CredentialTypeDetails from './CredentialTypeDetails';
|
import CredentialTypeDetails from './CredentialTypeDetails';
|
||||||
import CredentialTypeEdit from './CredentialTypeEdit';
|
import CredentialTypeEdit from './CredentialTypeEdit';
|
||||||
|
|
||||||
function CredentialType() {
|
function CredentialType({ i18n, setBreadcrumb }) {
|
||||||
|
const { id } = useParams();
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
error: contentError,
|
||||||
|
request: fetchCredentialTypes,
|
||||||
|
result: credentialType,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await CredentialTypesAPI.readDetail(id);
|
||||||
|
return data;
|
||||||
|
}, [id])
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCredentialTypes();
|
||||||
|
}, [fetchCredentialTypes, pathname]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (credentialType) {
|
||||||
|
setBreadcrumb(credentialType);
|
||||||
|
}
|
||||||
|
}, [credentialType, setBreadcrumb]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{i18n._(t`Back to credential types`)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: '/credential_types',
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Details`),
|
||||||
|
link: `/credential_types/${id}/details`,
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let cardHeader = <RoutedTabs tabsArray={tabsArray} />;
|
||||||
|
if (pathname.endsWith('edit')) {
|
||||||
|
cardHeader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && contentError) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentError error={contentError}>
|
||||||
|
{contentError.response?.status === 404 && (
|
||||||
|
<span>
|
||||||
|
{i18n._(t`Credential type not found.`)}{' '}
|
||||||
|
<Link to="/credential_types">
|
||||||
|
{i18n._(t`View all credential types`)}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<PageSection>
|
||||||
<Redirect
|
<Card>
|
||||||
from="/credential_types/:id"
|
{cardHeader}
|
||||||
to="/credential_types/:id/details"
|
{isLoading && <ContentLoading />}
|
||||||
exact
|
{!isLoading && credentialType && (
|
||||||
/>
|
<Switch>
|
||||||
<Route path="/credential_types/:id/edit">
|
<Redirect
|
||||||
<CredentialTypeEdit />
|
from="/credential_types/:id"
|
||||||
</Route>
|
to="/credential_types/:id/details"
|
||||||
<Route path="/credential_types/:id/details">
|
exact
|
||||||
<CredentialTypeDetails />
|
/>
|
||||||
</Route>
|
{credentialType && (
|
||||||
</Switch>
|
<>
|
||||||
|
<Route path="/credential_types/:id/edit">
|
||||||
|
<CredentialTypeEdit />
|
||||||
|
</Route>
|
||||||
|
<Route path="/credential_types/:id/details">
|
||||||
|
<CredentialTypeDetails credentialType={credentialType} />
|
||||||
|
</Route>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CredentialType;
|
export default withI18n()(CredentialType);
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
import { CredentialTypesAPI } from '../../api';
|
||||||
|
|
||||||
|
import CredentialType from './CredentialType';
|
||||||
|
|
||||||
|
jest.mock('../../api');
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useRouteMatch: () => ({
|
||||||
|
url: '/credential_types',
|
||||||
|
}),
|
||||||
|
useParams: () => ({ id: 42 }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('<CredentialType/>', () => {
|
||||||
|
let wrapper;
|
||||||
|
test('should render details properly', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<CredentialType setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('CredentialType').length).toBe(1);
|
||||||
|
expect(CredentialTypesAPI.readDetail).toBeCalledWith(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', async () => {
|
||||||
|
const expectedTabs = ['Back to credential types', 'Details'];
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<CredentialType setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/credential_types/42/foobar'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<CredentialType setBreadcrumb={() => {}} />, {
|
||||||
|
context: {
|
||||||
|
router: {
|
||||||
|
history,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,12 +1,103 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
import { Button } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import { VariablesDetail } from '../../../components/CodeMirrorInput';
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
|
import DeleteButton from '../../../components/DeleteButton';
|
||||||
|
import {
|
||||||
|
Detail,
|
||||||
|
DetailList,
|
||||||
|
UserDateDetail,
|
||||||
|
} from '../../../components/DetailList';
|
||||||
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
import { CredentialTypesAPI } from '../../../api';
|
||||||
|
import { jsonToYaml } from '../../../util/yaml';
|
||||||
|
|
||||||
|
function CredentialTypeDetails({ credentialType, i18n }) {
|
||||||
|
const { id, name, description, injectors, inputs } = credentialType;
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const {
|
||||||
|
request: deleteCredentialType,
|
||||||
|
isLoading,
|
||||||
|
error: deleteError,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
await CredentialTypesAPI.destroy(id);
|
||||||
|
history.push(`/credential_types`);
|
||||||
|
}, [id, history])
|
||||||
|
);
|
||||||
|
|
||||||
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
|
||||||
function CredentialTypeDetails() {
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<CardBody>
|
||||||
<Card>Credential Type Details</Card>
|
<DetailList>
|
||||||
</PageSection>
|
<Detail
|
||||||
|
label={i18n._(t`Name`)}
|
||||||
|
value={name}
|
||||||
|
dataCy="credential-type-detail-name"
|
||||||
|
/>
|
||||||
|
<Detail label={i18n._(t`Description`)} value={description} />
|
||||||
|
<VariablesDetail
|
||||||
|
label={i18n._(t`Input configuration`)}
|
||||||
|
value={jsonToYaml(JSON.stringify(inputs))}
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
<VariablesDetail
|
||||||
|
label={i18n._(t`Injector configuration`)}
|
||||||
|
value={jsonToYaml(JSON.stringify(injectors))}
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Created`)}
|
||||||
|
date={credentialType.created}
|
||||||
|
user={credentialType.summary_fields.created_by}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Last Modified`)}
|
||||||
|
date={credentialType.modified}
|
||||||
|
user={credentialType.summary_fields.modified_by}
|
||||||
|
/>
|
||||||
|
</DetailList>
|
||||||
|
<CardActionsRow>
|
||||||
|
{credentialType.summary_fields.user_capabilities &&
|
||||||
|
credentialType.summary_fields.user_capabilities.edit && (
|
||||||
|
<Button
|
||||||
|
aria-label={i18n._(t`edit`)}
|
||||||
|
component={Link}
|
||||||
|
to={`credential_types/${id}/edit`}
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{credentialType.summary_fields.user_capabilities &&
|
||||||
|
credentialType.summary_fields.user_capabilities.delete && (
|
||||||
|
<DeleteButton
|
||||||
|
name={name}
|
||||||
|
modalTitle={i18n._(t`Delete credential type`)}
|
||||||
|
onConfirm={deleteCredentialType}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</DeleteButton>
|
||||||
|
)}
|
||||||
|
</CardActionsRow>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={error}
|
||||||
|
onClose={dismissError}
|
||||||
|
title={i18n._(t`Error`)}
|
||||||
|
variant="error"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CredentialTypeDetails;
|
export default withI18n()(CredentialTypeDetails);
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import { CredentialTypesAPI } from '../../../api';
|
||||||
|
import { jsonToYaml } from '../../../util/yaml';
|
||||||
|
|
||||||
|
import CredentialTypeDetails from './CredentialTypeDetails';
|
||||||
|
|
||||||
|
jest.mock('../../../api');
|
||||||
|
|
||||||
|
const credentialTypeData = {
|
||||||
|
name: 'Foo',
|
||||||
|
description: 'Bar',
|
||||||
|
kind: 'cloud',
|
||||||
|
inputs: {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: 'username',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Jenkins username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'password',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Jenkins password',
|
||||||
|
secret: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
required: ['username', 'password'],
|
||||||
|
},
|
||||||
|
injectors: {
|
||||||
|
extra_vars: {
|
||||||
|
Jenkins_password: '{{ password }}',
|
||||||
|
Jenkins_username: '{{ username }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
summary_fields: {
|
||||||
|
created_by: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
},
|
||||||
|
modified_by: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
},
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
delete: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: '2020-06-25T16:52:36.127008Z',
|
||||||
|
modified: '2020-06-25T16:52:36.127022Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
function expectDetailToMatch(wrapper, label, value) {
|
||||||
|
const detail = wrapper.find(`Detail[label="${label}"]`);
|
||||||
|
expect(detail).toHaveLength(1);
|
||||||
|
expect(detail.prop('value')).toEqual(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('<CredentialTypeDetails/>', () => {
|
||||||
|
let wrapper;
|
||||||
|
test('should render details properly', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<CredentialTypeDetails credentialType={credentialTypeData} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expectDetailToMatch(wrapper, 'Name', credentialTypeData.name);
|
||||||
|
expectDetailToMatch(wrapper, 'Description', credentialTypeData.description);
|
||||||
|
const dates = wrapper.find('UserDateDetail');
|
||||||
|
expect(dates).toHaveLength(2);
|
||||||
|
expect(dates.at(0).prop('date')).toEqual(credentialTypeData.created);
|
||||||
|
expect(dates.at(1).prop('date')).toEqual(credentialTypeData.modified);
|
||||||
|
const vars = wrapper.find('VariablesDetail');
|
||||||
|
expect(vars).toHaveLength(2);
|
||||||
|
|
||||||
|
expect(vars.at(0).prop('label')).toEqual('Input configuration');
|
||||||
|
expect(vars.at(0).prop('value')).toEqual(
|
||||||
|
jsonToYaml(JSON.stringify(credentialTypeData.inputs))
|
||||||
|
);
|
||||||
|
expect(vars.at(1).prop('label')).toEqual('Injector configuration');
|
||||||
|
expect(vars.at(1).prop('value')).toEqual(
|
||||||
|
jsonToYaml(JSON.stringify(credentialTypeData.injectors))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expected api call is made for delete', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/credential_types/42/details'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<CredentialTypeDetails credentialType={credentialTypeData} />,
|
||||||
|
{
|
||||||
|
context: { router: { history } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('DeleteButton').invoke('onConfirm')();
|
||||||
|
});
|
||||||
|
expect(CredentialTypesAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(history.location.pathname).toBe('/credential_types');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not render delete button', async () => {
|
||||||
|
credentialTypeData.summary_fields.user_capabilities.delete = false;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<CredentialTypeDetails credentialType={credentialTypeData} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(0);
|
||||||
|
});
|
||||||
|
test('should not render edit button', async () => {
|
||||||
|
credentialTypeData.summary_fields.user_capabilities.edit = false;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<CredentialTypeDetails credentialType={credentialTypeData} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,6 +15,7 @@ import PaginatedDataList, {
|
|||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import DatalistToolbar from '../../../components/DataListToolbar';
|
import DatalistToolbar from '../../../components/DataListToolbar';
|
||||||
|
|
||||||
import CredentialTypeListItem from './CredentialTypeListItem';
|
import CredentialTypeListItem from './CredentialTypeListItem';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('credential_type', {
|
const QS_CONFIG = getQSConfig('credential_type', {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function CredentialTypeListItem({
|
|||||||
key="name"
|
key="name"
|
||||||
aria-label={i18n._(t`credential type name`)}
|
aria-label={i18n._(t`credential type name`)}
|
||||||
>
|
>
|
||||||
<Link to={`${detailUrl} `}>
|
<Link to={`${detailUrl}`}>
|
||||||
<b>{credentialType.name}</b>
|
<b>{credentialType.name}</b>
|
||||||
</Link>
|
</Link>
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ function CredentialTypes({ i18n }) {
|
|||||||
'/credential_types': i18n._(t`Credential Types`),
|
'/credential_types': i18n._(t`Credential Types`),
|
||||||
'/credential_types/add': i18n._(t`Create Credential Types`),
|
'/credential_types/add': i18n._(t`Create Credential Types`),
|
||||||
[`/credential_types/${credentialTypes.id}`]: `${credentialTypes.name}`,
|
[`/credential_types/${credentialTypes.id}`]: `${credentialTypes.name}`,
|
||||||
|
[`/credential_types/${credentialTypes.id}/edit`]: i18n._(
|
||||||
|
t`Edit Details`
|
||||||
|
),
|
||||||
|
[`/credential_types/${credentialTypes.id}/details`]: i18n._(t`Details`),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[i18n]
|
[i18n]
|
||||||
|
|||||||
Reference in New Issue
Block a user