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 && (