diff --git a/awx/ui_next/src/api/models/Credentials.js b/awx/ui_next/src/api/models/Credentials.js
index 13ee1f8a9c..1560357bd1 100644
--- a/awx/ui_next/src/api/models/Credentials.js
+++ b/awx/ui_next/src/api/models/Credentials.js
@@ -20,10 +20,38 @@ class Credentials extends Base {
return this.http.options(`${this.baseUrl}${id}/access_list/`);
}
- readInputSources(id, params) {
- return this.http.get(`${this.baseUrl}${id}/input_sources/`, {
- params,
- });
+ readInputSources(id) {
+ const maxRequests = 5;
+ let requestCounter = 0;
+ const fetchInputSources = async (pageNo = 1, inputSources = []) => {
+ try {
+ requestCounter++;
+ const { data } = await this.http.get(
+ `${this.baseUrl}${id}/input_sources/`,
+ {
+ params: {
+ page: pageNo,
+ page_size: 200,
+ },
+ }
+ );
+ if (data?.next && requestCounter <= maxRequests) {
+ return fetchInputSources(
+ pageNo + 1,
+ inputSources.concat(data.results)
+ );
+ }
+ return Promise.resolve({
+ data: {
+ results: inputSources.concat(data.results),
+ },
+ });
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ };
+
+ return fetchInputSources();
}
test(id, data) {
diff --git a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx
index 2e8809955f..5f8066b904 100644
--- a/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx
+++ b/awx/ui_next/src/screens/Credential/CredentialDetail/CredentialDetail.jsx
@@ -1,9 +1,9 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { Fragment, useEffect, useCallback } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { shape } from 'prop-types';
-
+import styled from 'styled-components';
import { Button, List, ListItem } from '@patternfly/react-core';
import AlertModal from '../../../components/AlertModal';
import { CardBody, CardActionsRow } from '../../../components/Card';
@@ -11,15 +11,26 @@ import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading';
import DeleteButton from '../../../components/DeleteButton';
import {
- DetailList,
Detail,
+ DetailList,
UserDateDetail,
} from '../../../components/DetailList';
+import ChipGroup from '../../../components/ChipGroup';
+import CodeMirrorInput from '../../../components/CodeMirrorInput';
+import CredentialChip from '../../../components/CredentialChip';
import ErrorDetail from '../../../components/ErrorDetail';
import { CredentialsAPI, CredentialTypesAPI } from '../../../api';
import { Credential } from '../../../types';
import useRequest, { useDismissableError } from '../../../util/useRequest';
+const PluginInputMetadata = styled(CodeMirrorInput)`
+ grid-column: 1 / -1;
+`;
+
+const PluginFieldText = styled.p`
+ margin-top: 10px;
+`;
+
function CredentialDetail({ i18n, credential }) {
const {
id: credentialId,
@@ -36,31 +47,44 @@ function CredentialDetail({ i18n, credential }) {
user_capabilities,
},
} = credential;
-
- const [fields, setFields] = useState([]);
- const [managedByTower, setManagedByTower] = useState([]);
- const [contentError, setContentError] = useState(null);
- const [hasContentLoading, setHasContentLoading] = useState(true);
const history = useHistory();
- useEffect(() => {
- (async () => {
- setContentError(null);
- setHasContentLoading(true);
- try {
- const {
+ const {
+ result: { fields, managedByTower, inputSources },
+ request: fetchDetails,
+ isLoading: hasContentLoading,
+ error: contentError,
+ } = useRequest(
+ useCallback(async () => {
+ const [
+ {
data: { inputs: credentialTypeInputs, managed_by_tower },
- } = await CredentialTypesAPI.readDetail(credential_type.id);
-
- setFields(credentialTypeInputs.fields || []);
- setManagedByTower(managed_by_tower);
- } catch (error) {
- setContentError(error);
- } finally {
- setHasContentLoading(false);
- }
- })();
- }, [credential_type]);
+ },
+ {
+ data: { results: loadedInputSources },
+ },
+ ] = await Promise.all([
+ CredentialTypesAPI.readDetail(credential_type.id),
+ CredentialsAPI.readInputSources(credentialId),
+ ]);
+ return {
+ fields: credentialTypeInputs.fields || [],
+ managedByTower: managed_by_tower,
+ inputSources: loadedInputSources.reduce(
+ (inputSourcesMap, inputSource) => {
+ inputSourcesMap[inputSource.input_field_name] = inputSource;
+ return inputSourcesMap;
+ },
+ {}
+ ),
+ };
+ }, [credentialId, credential_type]),
+ {
+ fields: [],
+ managedByTower: true,
+ inputSources: {},
+ }
+ );
const {
request: deleteCredential,
@@ -75,34 +99,84 @@ function CredentialDetail({ i18n, credential }) {
const { error, dismissError } = useDismissableError(deleteError);
- const renderDetail = ({ id, label, type }) => {
- let detail;
+ const renderDetail = ({ id, label, type, ask_at_runtime }) => {
+ if (inputSources[id]) {
+ return (
+
+ {label} *}
+ value={
+
+
+
+ }
+ />
+ {}}
+ rows={5}
+ hasErrors={false}
+ />
+
+ );
+ }
if (type === 'boolean') {
- detail = (
+ return (
{inputs[id] && {label}}}
/>
);
- } else if (inputs[id] === '$encrypted$') {
- const isEncrypted = true;
- detail = (
+ }
+
+ if (inputs[id] === '$encrypted$') {
+ return (
);
- } else {
- detail = ;
}
- return detail;
+ if (ask_at_runtime && inputs[id] === 'ASK') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
};
+ useEffect(() => {
+ fetchDetails();
+ }, [fetchDetails]);
+
if (hasContentLoading) {
return ;
}
@@ -114,10 +188,19 @@ function CredentialDetail({ i18n, credential }) {
return (
-
-
+
+
{organization && (
@@ -127,6 +210,7 @@ function CredentialDetail({ i18n, credential }) {
/>
)}
renderDetail(field))}
+ {Object.keys(inputSources).length > 0 && (
+
+ {i18n._(
+ t`* This field will be retrieved from an external secret management system using the specified credential.`
+ )}
+
+ )}
{user_capabilities.edit && (