Merge pull request #9815 from keithjgrant/4837-docs-links

Create single source of truth for outgoing Docs link URLs

SUMMARY
Adds a utility function to generate links to the correct version of documentation and updates/adds a number of docs links throughout the app.
Addresses #8428
ISSUE TYPE

Feature Pull Request

COMPONENT NAME

UI

Reviewed-by: Jake McDermott <yo@jakemcdermott.me>
This commit is contained in:
softwarefactory-project-zuul[bot]
2021-04-06 19:22:19 +00:00
committed by GitHub
20 changed files with 280 additions and 144 deletions

View File

@@ -78,7 +78,8 @@
"src", "src",
"theme", "theme",
"gridColumns", "gridColumns",
"rows" "rows",
"href"
], ],
"ignore": ["Ansible", "Tower", "JSON", "YAML", "lg"], "ignore": ["Ansible", "Tower", "JSON", "YAML", "lg"],
"ignoreComponent": [ "ignoreComponent": [

View File

@@ -22,6 +22,8 @@ import {
} from '@patternfly/react-icons'; } from '@patternfly/react-icons';
import { WorkflowApprovalsAPI } from '../../api'; import { WorkflowApprovalsAPI } from '../../api';
import useRequest from '../../util/useRequest'; import useRequest from '../../util/useRequest';
import getDocsBaseUrl from '../../util/getDocsBaseUrl';
import { useConfig } from '../../contexts/Config';
import useWsPendingApprovalCount from './useWsPendingApprovalCount'; import useWsPendingApprovalCount from './useWsPendingApprovalCount';
const PendingWorkflowApprovals = styled.div` const PendingWorkflowApprovals = styled.div`
@@ -35,9 +37,6 @@ const PendingWorkflowApprovalBadge = styled(Badge)`
margin-left: 10px; margin-left: 10px;
`; `;
const DOCLINK =
'https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html';
function PageHeaderToolbar({ function PageHeaderToolbar({
isAboutDisabled, isAboutDisabled,
onAboutClick, onAboutClick,
@@ -47,6 +46,7 @@ function PageHeaderToolbar({
}) { }) {
const [isHelpOpen, setIsHelpOpen] = useState(false); const [isHelpOpen, setIsHelpOpen] = useState(false);
const [isUserOpen, setIsUserOpen] = useState(false); const [isUserOpen, setIsUserOpen] = useState(false);
const config = useConfig();
const { const {
request: fetchPendingApprovalCount, request: fetchPendingApprovalCount,
@@ -101,37 +101,39 @@ function PageHeaderToolbar({
</Link> </Link>
</PageHeaderToolsItem> </PageHeaderToolsItem>
</Tooltip> </Tooltip>
<Tooltip position="bottom" content={<div>{i18n._(t`Info`)}</div>}> <PageHeaderToolsItem>
<PageHeaderToolsItem> <Dropdown
<Dropdown isPlain
isPlain isOpen={isHelpOpen}
isOpen={isHelpOpen} position={DropdownPosition.right}
position={DropdownPosition.right} onSelect={handleHelpSelect}
onSelect={handleHelpSelect} toggle={
toggle={ <DropdownToggle
<DropdownToggle onToggle={setIsHelpOpen}
onToggle={setIsHelpOpen} aria-label={i18n._(t`Info`)}
aria-label={i18n._(t`Info`)} >
> <QuestionCircleIcon />
<QuestionCircleIcon /> </DropdownToggle>
</DropdownToggle> }
} dropdownItems={[
dropdownItems={[ <DropdownItem
<DropdownItem key="help" target="_blank" href={DOCLINK}> key="help"
{i18n._(t`Help`)} target="_blank"
</DropdownItem>, href={`${getDocsBaseUrl(config)}//html/userguide/index.html`}
<DropdownItem >
key="about" {i18n._(t`Help`)}
component="button" </DropdownItem>,
isDisabled={isAboutDisabled} <DropdownItem
onClick={onAboutClick} key="about"
> component="button"
{i18n._(t`About`)} isDisabled={isAboutDisabled}
</DropdownItem>, onClick={onAboutClick}
]} >
/> {i18n._(t`About`)}
</PageHeaderToolsItem> </DropdownItem>,
</Tooltip> ]}
/>
</PageHeaderToolsItem>
<Tooltip position="left" content={<div>{i18n._(t`User`)}</div>}> <Tooltip position="left" content={<div>{i18n._(t`User`)}</div>}>
<PageHeaderToolsItem> <PageHeaderToolsItem>
<Dropdown <Dropdown

View File

@@ -0,0 +1 @@
export { default } from './DocsLink';

View File

@@ -11,9 +11,12 @@ import {
SelectOption, SelectOption,
SelectVariant, SelectVariant,
TextInput, TextInput,
Tooltip,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons'; import { SearchIcon, QuestionCircleIcon } from '@patternfly/react-icons';
import styled from 'styled-components'; import styled from 'styled-components';
import { useConfig } from '../../contexts/Config';
import getDocsBaseUrl from '../../util/getDocsBaseUrl';
const AdvancedGroup = styled.div` const AdvancedGroup = styled.div`
display: flex; display: flex;
@@ -45,6 +48,7 @@ function AdvancedSearch({
const [lookupSelection, setLookupSelection] = useState(null); const [lookupSelection, setLookupSelection] = useState(null);
const [keySelection, setKeySelection] = useState(null); const [keySelection, setKeySelection] = useState(null);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const config = useConfig();
const handleAdvancedSearch = e => { const handleAdvancedSearch = e => {
// keeps page from fully reloading // keeps page from fully reloading
@@ -262,6 +266,19 @@ function AdvancedSearch({
</Button> </Button>
</div> </div>
</InputGroup> </InputGroup>
<Tooltip
content={i18n._(t`Advanced search documentation`)}
position="bottom"
>
<Button
component="a"
variant="plain"
target="_blank"
href={`${getDocsBaseUrl(config)}/html/userguide/search_sort.html`}
>
<QuestionCircleIcon />
</Button>
</Tooltip>
</AdvancedGroup> </AdvancedGroup>
); );
} }

View File

@@ -12,6 +12,8 @@ import {
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import { required } from '../../../../util/validators'; import { required } from '../../../../util/validators';
import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
import { useConfig } from '../../../../contexts/Config';
const AzureSubForm = ({ autoPopulateCredential, i18n }) => { const AzureSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
@@ -19,6 +21,7 @@ const AzureSubForm = ({ autoPopulateCredential, i18n }) => {
name: 'credential', name: 'credential',
validate: required(i18n._(t`Select a value for this field`), i18n), validate: required(i18n._(t`Select a value for this field`), i18n),
}); });
const config = useConfig();
const handleCredentialUpdate = useCallback( const handleCredentialUpdate = useCallback(
value => { value => {
@@ -27,8 +30,9 @@ const AzureSubForm = ({ autoPopulateCredential, i18n }) => {
[setFieldValue] [setFieldValue]
); );
const pluginLink = const pluginLink = `${getDocsBaseUrl(
'http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins'; config
)}/html/userguide/inventories.html#inventory-plugins`;
const configLink = const configLink =
'https://docs.ansible.com/ansible/latest/collections/azure/azcollection/azure_rm_inventory.html'; 'https://docs.ansible.com/ansible/latest/collections/azure/azcollection/azure_rm_inventory.html';

View File

@@ -11,10 +11,13 @@ import {
EnabledValueField, EnabledValueField,
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
import { useConfig } from '../../../../contexts/Config';
const EC2SubForm = ({ i18n }) => { const EC2SubForm = ({ i18n }) => {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
const [credentialField] = useField('credential'); const [credentialField] = useField('credential');
const config = useConfig();
const handleCredentialUpdate = useCallback( const handleCredentialUpdate = useCallback(
value => { value => {
@@ -23,8 +26,9 @@ const EC2SubForm = ({ i18n }) => {
[setFieldValue] [setFieldValue]
); );
const pluginLink = const pluginLink = `${getDocsBaseUrl(
'http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins'; config
)}/html/userguide/inventories.html#inventory-plugins`;
const configLink = const configLink =
'https://docs.ansible.com/ansible/latest/collections/amazon/aws/aws_ec2_inventory.html'; 'https://docs.ansible.com/ansible/latest/collections/amazon/aws/aws_ec2_inventory.html';

View File

@@ -12,6 +12,8 @@ import {
SourceVarsField, SourceVarsField,
} from './SharedFields'; } from './SharedFields';
import { required } from '../../../../util/validators'; import { required } from '../../../../util/validators';
import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
import { useConfig } from '../../../../contexts/Config';
const GCESubForm = ({ autoPopulateCredential, i18n }) => { const GCESubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
@@ -19,6 +21,7 @@ const GCESubForm = ({ autoPopulateCredential, i18n }) => {
name: 'credential', name: 'credential',
validate: required(i18n._(t`Select a value for this field`), i18n), validate: required(i18n._(t`Select a value for this field`), i18n),
}); });
const config = useConfig();
const handleCredentialUpdate = useCallback( const handleCredentialUpdate = useCallback(
value => { value => {
@@ -27,8 +30,9 @@ const GCESubForm = ({ autoPopulateCredential, i18n }) => {
[setFieldValue] [setFieldValue]
); );
const pluginLink = const pluginLink = `${getDocsBaseUrl(
'http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins'; config
)}/html/userguide/inventories.html#inventory-plugins`;
const configLink = const configLink =
'https://docs.ansible.com/ansible/latest/collections/google/cloud/gcp_compute_inventory.html'; 'https://docs.ansible.com/ansible/latest/collections/google/cloud/gcp_compute_inventory.html';

View File

@@ -12,6 +12,8 @@ import {
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import { required } from '../../../../util/validators'; import { required } from '../../../../util/validators';
import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
import { useConfig } from '../../../../contexts/Config';
const OpenStackSubForm = ({ autoPopulateCredential, i18n }) => { const OpenStackSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
@@ -19,6 +21,7 @@ const OpenStackSubForm = ({ autoPopulateCredential, i18n }) => {
name: 'credential', name: 'credential',
validate: required(i18n._(t`Select a value for this field`), i18n), validate: required(i18n._(t`Select a value for this field`), i18n),
}); });
const config = useConfig();
const handleCredentialUpdate = useCallback( const handleCredentialUpdate = useCallback(
value => { value => {
@@ -27,8 +30,9 @@ const OpenStackSubForm = ({ autoPopulateCredential, i18n }) => {
[setFieldValue] [setFieldValue]
); );
const pluginLink = const pluginLink = `${getDocsBaseUrl(
'http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins'; config
)}/html/userguide/inventories.html#inventory-plugins`;
const configLink = const configLink =
'https://docs.ansible.com/ansible/latest/collections/openstack/cloud/openstack_inventory.html'; 'https://docs.ansible.com/ansible/latest/collections/openstack/cloud/openstack_inventory.html';

View File

@@ -12,6 +12,8 @@ import {
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import { required } from '../../../../util/validators'; import { required } from '../../../../util/validators';
import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
import { useConfig } from '../../../../contexts/Config';
const SatelliteSubForm = ({ autoPopulateCredential, i18n }) => { const SatelliteSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
@@ -19,6 +21,7 @@ const SatelliteSubForm = ({ autoPopulateCredential, i18n }) => {
name: 'credential', name: 'credential',
validate: required(i18n._(t`Select a value for this field`), i18n), validate: required(i18n._(t`Select a value for this field`), i18n),
}); });
const config = useConfig();
const handleCredentialUpdate = useCallback( const handleCredentialUpdate = useCallback(
value => { value => {
@@ -27,8 +30,9 @@ const SatelliteSubForm = ({ autoPopulateCredential, i18n }) => {
[setFieldValue] [setFieldValue]
); );
const pluginLink = const pluginLink = `${getDocsBaseUrl(
'http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins'; config
)}/html/userguide/inventories.html#inventory-plugins`;
const configLink = const configLink =
'https://docs.ansible.com/ansible/latest/collections/theforeman/foreman/foreman_inventory.html'; 'https://docs.ansible.com/ansible/latest/collections/theforeman/foreman/foreman_inventory.html';

View File

@@ -12,6 +12,8 @@ import {
SourceVarsField, SourceVarsField,
} from './SharedFields'; } from './SharedFields';
import { required } from '../../../../util/validators'; import { required } from '../../../../util/validators';
import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
import { useConfig } from '../../../../contexts/Config';
const TowerSubForm = ({ autoPopulateCredential, i18n }) => { const TowerSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
@@ -19,6 +21,7 @@ const TowerSubForm = ({ autoPopulateCredential, i18n }) => {
name: 'credential', name: 'credential',
validate: required(i18n._(t`Select a value for this field`), i18n), validate: required(i18n._(t`Select a value for this field`), i18n),
}); });
const config = useConfig();
const handleCredentialUpdate = useCallback( const handleCredentialUpdate = useCallback(
value => { value => {
@@ -27,8 +30,9 @@ const TowerSubForm = ({ autoPopulateCredential, i18n }) => {
[setFieldValue] [setFieldValue]
); );
const pluginLink = const pluginLink = `${getDocsBaseUrl(
'http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins'; config
)}/html/userguide/inventories.html#inventory-plugins`;
const configLink = const configLink =
'https://docs.ansible.com/ansible/latest/collections/awx/awx/tower_inventory.html'; 'https://docs.ansible.com/ansible/latest/collections/awx/awx/tower_inventory.html';

View File

@@ -12,6 +12,8 @@ import {
HostFilterField, HostFilterField,
} from './SharedFields'; } from './SharedFields';
import { required } from '../../../../util/validators'; import { required } from '../../../../util/validators';
import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
import { useConfig } from '../../../../contexts/Config';
const VMwareSubForm = ({ autoPopulateCredential, i18n }) => { const VMwareSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
@@ -19,6 +21,7 @@ const VMwareSubForm = ({ autoPopulateCredential, i18n }) => {
name: 'credential', name: 'credential',
validate: required(i18n._(t`Select a value for this field`), i18n), validate: required(i18n._(t`Select a value for this field`), i18n),
}); });
const config = useConfig();
const handleCredentialUpdate = useCallback( const handleCredentialUpdate = useCallback(
value => { value => {
@@ -27,8 +30,9 @@ const VMwareSubForm = ({ autoPopulateCredential, i18n }) => {
[setFieldValue] [setFieldValue]
); );
const pluginLink = const pluginLink = `${getDocsBaseUrl(
'http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins'; config
)}/html/userguide/inventories.html#inventory-plugins`;
const configLink = const configLink =
'https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_vm_inventory_inventory.html'; 'https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_vm_inventory_inventory.html';

View File

@@ -12,6 +12,8 @@ import {
SourceVarsField, SourceVarsField,
} from './SharedFields'; } from './SharedFields';
import { required } from '../../../../util/validators'; import { required } from '../../../../util/validators';
import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
import { useConfig } from '../../../../contexts/Config';
const VirtualizationSubForm = ({ autoPopulateCredential, i18n }) => { const VirtualizationSubForm = ({ autoPopulateCredential, i18n }) => {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
@@ -19,6 +21,7 @@ const VirtualizationSubForm = ({ autoPopulateCredential, i18n }) => {
name: 'credential', name: 'credential',
validate: required(i18n._(t`Select a value for this field`), i18n), validate: required(i18n._(t`Select a value for this field`), i18n),
}); });
const config = useConfig();
const handleCredentialUpdate = useCallback( const handleCredentialUpdate = useCallback(
value => { value => {
@@ -27,8 +30,9 @@ const VirtualizationSubForm = ({ autoPopulateCredential, i18n }) => {
[setFieldValue] [setFieldValue]
); );
const pluginLink = const pluginLink = `${getDocsBaseUrl(
'http://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#inventory-plugins'; config
)}/html/userguide/inventories.html#inventory-plugins`;
const configLink = const configLink =
'https://docs.ansible.com/ansible/latest/collections/ovirt/ovirt/ovirt_inventory.html'; 'https://docs.ansible.com/ansible/latest/collections/ovirt/ovirt/ovirt_inventory.html';

View File

@@ -21,7 +21,7 @@ import {
ToolbarToggleGroup, ToolbarToggleGroup,
Tooltip, Tooltip,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons'; import { SearchIcon, QuestionCircleIcon } from '@patternfly/react-icons';
import AlertModal from '../../../components/AlertModal'; import AlertModal from '../../../components/AlertModal';
import { CardBody as _CardBody } from '../../../components/Card'; import { CardBody as _CardBody } from '../../../components/Card';
@@ -47,6 +47,8 @@ import {
removeParams, removeParams,
getQSConfig, getQSConfig,
} from '../../../util/qs'; } from '../../../util/qs';
import getDocsBaseUrl from '../../../util/getDocsBaseUrl';
import { useConfig } from '../../../contexts/Config';
const QS_CONFIG = getQSConfig('job_output', { const QS_CONFIG = getQSConfig('job_output', {
order_by: 'start_line', order_by: 'start_line',
@@ -280,6 +282,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
const jobSocketCounter = useRef(0); const jobSocketCounter = useRef(0);
const interval = useRef(null); const interval = useRef(null);
const history = useHistory(); const history = useHistory();
const config = useConfig();
const [contentError, setContentError] = useState(null); const [contentError, setContentError] = useState(null);
const [cssMap, setCssMap] = useState({}); const [cssMap, setCssMap] = useState({});
const [currentlyLoading, setCurrentlyLoading] = useState([]); const [currentlyLoading, setCurrentlyLoading] = useState([]);
@@ -730,6 +733,21 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
) : ( ) : (
renderSearchComponent(i18n) renderSearchComponent(i18n)
)} )}
<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> </ToolbarItem>
</ToolbarToggleGroup> </ToolbarToggleGroup>
</SearchToolbarContent> </SearchToolbarContent>

View File

@@ -1,6 +1,6 @@
import 'styled-components/macro'; import 'styled-components/macro';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { Trans, withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useField, useFormikContext } from 'formik'; import { useField, useFormikContext } from 'formik';
import { Switch, Text } from '@patternfly/react-core'; import { Switch, Text } from '@patternfly/react-core';
@@ -9,6 +9,8 @@ import {
SubFormLayout, SubFormLayout,
} from '../../../components/FormLayout'; } from '../../../components/FormLayout';
import CodeEditorField from '../../../components/CodeEditor/CodeEditorField'; import CodeEditorField from '../../../components/CodeEditor/CodeEditorField';
import { useConfig } from '../../../contexts/Config';
import getDocsBaseUrl from '../../../util/getDocsBaseUrl';
function CustomMessagesSubForm({ defaultMessages, type, i18n }) { function CustomMessagesSubForm({ defaultMessages, type, i18n }) {
const [useCustomField, , useCustomHelpers] = useField('useCustomMessages'); const [useCustomField, , useCustomHelpers] = useField('useCustomMessages');
@@ -16,6 +18,7 @@ function CustomMessagesSubForm({ defaultMessages, type, i18n }) {
const showBodies = ['email', 'pagerduty', 'webhook'].includes(type); const showBodies = ['email', 'pagerduty', 'webhook'].includes(type);
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
const config = useConfig();
const mountedRef = useRef(null); const mountedRef = useRef(null);
useEffect( useEffect(
function resetToDefaultMessages() { function resetToDefaultMessages() {
@@ -69,11 +72,9 @@ function CustomMessagesSubForm({ defaultMessages, type, i18n }) {
css="margin-bottom: var(--pf-c-content--MarginBottom)" css="margin-bottom: var(--pf-c-content--MarginBottom)"
> >
<small> <small>
<Trans> {i18n._(t`Use custom messages to change the content of
Use custom messages to change the content of notifications sent notifications sent when a job starts, succeeds, or fails. Use
when a job starts, succeeds, or fails. Use curly braces to curly braces to access information about the job:`)}{' '}
access information about the job:{' '}
</Trans>
<code> <code>
{'{{'} job_friendly_name {'}}'} {'{{'} job_friendly_name {'}}'}
</code> </code>
@@ -81,23 +82,22 @@ function CustomMessagesSubForm({ defaultMessages, type, i18n }) {
<code> <code>
{'{{'} url {'}}'} {'{{'} url {'}}'}
</code> </code>
, <Trans>or attributes of the job such as</Trans>{' '} ,{' '}
<code> <code>
{'{{'} job.status {'}}'} {'{{'} job.status {'}}'}
</code> </code>
.{' '} .{' '}
<Trans> {i18n._(t`You may apply a number of possible variables in the
You may apply a number of possible variables in the message. message. For more information, refer to the`)}{' '}
Refer to the{' '}
</Trans>{' '}
<a <a
href="https://docs.ansible.com/ansible-tower/latest/html/userguide/notifications.html#create-custom-notifications"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
href={`${getDocsBaseUrl(
config
)}/html/userguide/notifications.html#create-custom-notifications`}
> >
Ansible Tower documentation {i18n._(t`Ansible Tower Documentation.`)}
</a>{' '} </a>
<Trans>for more details.</Trans>
</small> </small>
</Text> </Text>
<FormFullWidthLayout> <FormFullWidthLayout>

View File

@@ -9,90 +9,97 @@ import {
ScmCredentialFormField, ScmCredentialFormField,
ScmTypeOptions, ScmTypeOptions,
} from './SharedFields'; } from './SharedFields';
import { useConfig } from '../../../../contexts/Config';
import getDocsBaseUrl from '../../../../util/getDocsBaseUrl';
const GitSubForm = ({ const GitSubForm = ({
i18n, i18n,
credential, credential,
onCredentialSelection, onCredentialSelection,
scmUpdateOnLaunch, scmUpdateOnLaunch,
}) => ( }) => {
<> const config = useConfig();
<UrlFormField return (
i18n={i18n} <>
tooltip={ <UrlFormField
<span> i18n={i18n}
{i18n._(t`Example URLs for GIT Source Control include:`)} tooltip={
<ul css="margin: 10px 0 10px 20px"> <span>
<li> {i18n._(t`Example URLs for GIT Source Control include:`)}
<code>https://github.com/ansible/ansible.git</code> <ul css="margin: 10px 0 10px 20px">
</li> <li>
<li> <code>https://github.com/ansible/ansible.git</code>
<code>git@github.com:ansible/ansible.git</code> </li>
</li> <li>
<li> <code>git@github.com:ansible/ansible.git</code>
<code>git://servername.example.com/ansible.git</code> </li>
</li> <li>
</ul> <code>git://servername.example.com/ansible.git</code>
{i18n._(t`Note: When using SSH protocol for GitHub or </li>
</ul>
{i18n._(t`Note: When using SSH protocol for GitHub or
Bitbucket, enter an SSH key only, do not enter a username Bitbucket, enter an SSH key only, do not enter a username
(other than git). Additionally, GitHub and Bitbucket do (other than git). Additionally, GitHub and Bitbucket do
not support password authentication when using SSH. GIT not support password authentication when using SSH. GIT
read only protocol (git://) does not use username or read only protocol (git://) does not use username or
password information.`)} password information.`)}
</span> </span>
} }
/> />
<BranchFormField <BranchFormField
i18n={i18n} i18n={i18n}
label={i18n._(t`Source Control Branch/Tag/Commit`)} label={i18n._(t`Source Control Branch/Tag/Commit`)}
/> />
<FormField <FormField
id="project-scm-refspec" id="project-scm-refspec"
label={i18n._(t`Source Control Refspec`)} label={i18n._(t`Source Control Refspec`)}
name="scm_refspec" name="scm_refspec"
type="text" type="text"
tooltipMaxWidth="400px" tooltipMaxWidth="400px"
tooltip={ tooltip={
<span> <span>
{i18n._(t`A refspec to fetch (passed to the Ansible git {i18n._(t`A refspec to fetch (passed to the Ansible git
module). This parameter allows access to references via module). This parameter allows access to references via
the branch field not otherwise available.`)} the branch field not otherwise available.`)}
<br /> <br />
<br /> <br />
{i18n._(t`Note: This field assumes the remote name is "origin".`)} {i18n._(t`Note: This field assumes the remote name is "origin".`)}
<br /> <br />
<br /> <br />
{i18n._(t`Examples include:`)} {i18n._(t`Examples include:`)}
<ul css={{ margin: '10px 0 10px 20px' }}> <ul css={{ margin: '10px 0 10px 20px' }}>
<li> <li>
<code>refs/*:refs/remotes/origin/*</code> <code>refs/*:refs/remotes/origin/*</code>
</li> </li>
<li> <li>
<code>refs/pull/62/head:refs/remotes/origin/pull/62/head</code> <code>refs/pull/62/head:refs/remotes/origin/pull/62/head</code>
</li> </li>
</ul> </ul>
{i18n._(t`The first fetches all references. The second {i18n._(t`The first fetches all references. The second
fetches the Github pull request number 62, in this example fetches the Github pull request number 62, in this example
the branch needs to be "pull/62/head".`)} the branch needs to be "pull/62/head".`)}
<br /> <br />
<br /> <br />
{i18n._(t`For more information, refer to the`)}{' '} {i18n._(t`For more information, refer to the`)}{' '}
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
href="https://docs.ansible.com/ansible-tower/latest/html/userguide/projects.html#manage-playbooks-using-source-control" href={`${getDocsBaseUrl(
> config
{i18n._(t`Ansible Tower Documentation.`)} )}/html/userguide/projects.html#manage-playbooks-using-source-control`}
</a> >
</span> {i18n._(t`Ansible Tower Documentation.`)}
} </a>
/> </span>
<ScmCredentialFormField }
credential={credential} />
onCredentialSelection={onCredentialSelection} <ScmCredentialFormField
/> credential={credential}
<ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} /> onCredentialSelection={onCredentialSelection}
</> />
); <ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} />
</>
);
};
export default withI18n()(GitSubForm); export default withI18n()(GitSubForm);

View File

@@ -26,6 +26,8 @@ import JobTemplatesList from './JobTemplatesList';
import ProjectsList from './ProjectsList'; import ProjectsList from './ProjectsList';
import WorkflowJobTemplatesList from './WorkflowJobTemplatesList'; import WorkflowJobTemplatesList from './WorkflowJobTemplatesList';
import FormField from '../../../../../../components/FormField'; import FormField from '../../../../../../components/FormField';
import getDocsBaseUrl from '../../../../../../util/getDocsBaseUrl';
import { useConfig } from '../../../../../../contexts/Config';
const NodeTypeErrorAlert = styled(Alert)` const NodeTypeErrorAlert = styled(Alert)`
margin-bottom: 20px; margin-bottom: 20px;
@@ -59,6 +61,7 @@ function NodeTypeStep({ i18n }) {
const [convergenceField, , convergenceFieldHelpers] = useField('convergence'); const [convergenceField, , convergenceFieldHelpers] = useField('convergence');
const [isConvergenceOpen, setIsConvergenceOpen] = useState(false); const [isConvergenceOpen, setIsConvergenceOpen] = useState(false);
const config = useConfig();
const isValid = !approvalNameMeta.touched || !approvalNameMeta.error; const isValid = !approvalNameMeta.touched || !approvalNameMeta.error;
return ( return (
@@ -212,7 +215,9 @@ function NodeTypeStep({ i18n }) {
t`Preconditions for running this node when there are multiple parents. Refer to the` t`Preconditions for running this node when there are multiple parents. Refer to the`
)}{' '} )}{' '}
<a <a
href="https://docs.ansible.com/ansible-tower/latest/html/userguide/workflow_templates.html#convergence-node" href={`${getDocsBaseUrl(
config
)}/html/userguide/workflow_templates.html#convergence-node`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View File

@@ -23,6 +23,8 @@ import {
WorkflowDispatchContext, WorkflowDispatchContext,
WorkflowStateContext, WorkflowStateContext,
} from '../../../contexts/Workflow'; } from '../../../contexts/Workflow';
import getDocsBaseUrl from '../../../util/getDocsBaseUrl';
import { useConfig } from '../../../contexts/Config';
const Badge = styled(PFBadge)` const Badge = styled(PFBadge)`
align-items: center; align-items: center;
@@ -47,9 +49,6 @@ const ActionButton = styled(Button)`
`; `;
ActionButton.displayName = 'ActionButton'; ActionButton.displayName = 'ActionButton';
const DOCLINK =
'https://docs.ansible.com/ansible-tower/latest/html/userguide/workflow_templates.html#ug-wf-editor';
function VisualizerToolbar({ function VisualizerToolbar({
i18n, i18n,
onClose, onClose,
@@ -59,8 +58,8 @@ function VisualizerToolbar({
readOnly, readOnly,
}) { }) {
const dispatch = useContext(WorkflowDispatchContext); const dispatch = useContext(WorkflowDispatchContext);
const { nodes, showLegend, showTools } = useContext(WorkflowStateContext); const { nodes, showLegend, showTools } = useContext(WorkflowStateContext);
const config = useConfig();
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1; const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
@@ -113,7 +112,9 @@ function VisualizerToolbar({
variant="plain" variant="plain"
component="a" component="a"
target="_blank" target="_blank"
href={DOCLINK} href={`${getDocsBaseUrl(
config
)}/html/userguide/workflow_templates.html#ug-wf-editor`}
> >
<BookIcon /> <BookIcon />
</ActionButton> </ActionButton>

View File

@@ -0,0 +1,8 @@
export default function getDocsBaseUrl(config) {
let version = 'latest';
const licenseType = config?.license_info?.license_type;
if (licenseType && licenseType !== 'open') {
version = config?.version ? config.version.split('-')[0] : 'latest';
}
return `https://docs.ansible.com/ansible-tower/${version}`;
}

View File

@@ -0,0 +1,44 @@
import getDocsBaseUrl from './getDocsBaseUrl';
describe('getDocsBaseUrl', () => {
it('should return latest version for open license', () => {
const result = getDocsBaseUrl({
license_info: {
license_type: 'open',
},
version: '18.0.0',
});
expect(result).toEqual('https://docs.ansible.com/ansible-tower/latest');
});
it('should return current version for enterprise license', () => {
const result = getDocsBaseUrl({
license_info: {
license_type: 'enterprise',
},
version: '4.0.0',
});
expect(result).toEqual('https://docs.ansible.com/ansible-tower/4.0.0');
});
it('should strip version info after hyphen', () => {
const result = getDocsBaseUrl({
license_info: {
license_type: 'enterprise',
},
version: '4.0.0-beta',
});
expect(result).toEqual('https://docs.ansible.com/ansible-tower/4.0.0');
});
it('should return latest version if license info missing', () => {
const result = getDocsBaseUrl({
version: '18.0.0',
});
expect(result).toEqual('https://docs.ansible.com/ansible-tower/latest');
});
});