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,7 +101,6 @@ function PageHeaderToolbar({
</Link> </Link>
</PageHeaderToolsItem> </PageHeaderToolsItem>
</Tooltip> </Tooltip>
<Tooltip position="bottom" content={<div>{i18n._(t`Info`)}</div>}>
<PageHeaderToolsItem> <PageHeaderToolsItem>
<Dropdown <Dropdown
isPlain isPlain
@@ -117,7 +116,11 @@ function PageHeaderToolbar({
</DropdownToggle> </DropdownToggle>
} }
dropdownItems={[ dropdownItems={[
<DropdownItem key="help" target="_blank" href={DOCLINK}> <DropdownItem
key="help"
target="_blank"
href={`${getDocsBaseUrl(config)}//html/userguide/index.html`}
>
{i18n._(t`Help`)} {i18n._(t`Help`)}
</DropdownItem>, </DropdownItem>,
<DropdownItem <DropdownItem
@@ -131,7 +134,6 @@ function PageHeaderToolbar({
]} ]}
/> />
</PageHeaderToolsItem> </PageHeaderToolsItem>
</Tooltip>
<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,13 +9,17 @@ 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();
return (
<> <>
<UrlFormField <UrlFormField
i18n={i18n} i18n={i18n}
@@ -80,7 +84,9 @@ const GitSubForm = ({
<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
)}/html/userguide/projects.html#manage-playbooks-using-source-control`}
> >
{i18n._(t`Ansible Tower Documentation.`)} {i18n._(t`Ansible Tower Documentation.`)}
</a> </a>
@@ -93,6 +99,7 @@ const GitSubForm = ({
/> />
<ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} /> <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');
});
});