foreman_theme_satellite 16.4.2 → 16.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +12 -5
- data/app/assets/stylesheets/foreman_theme_satellite/documentation.scss +12 -0
- data/app/assets/stylesheets/foreman_theme_satellite/foreman_theme_satellite.css +1 -0
- data/app/controllers/concerns/documentation_controller_branding.rb +15 -4
- data/app/helpers/documentation_visibility_helper.rb +30 -0
- data/app/overrides/hide_documentation.rb +23 -0
- data/lib/foreman_theme_satellite/engine.rb +1 -0
- data/lib/foreman_theme_satellite/version.rb +1 -1
- data/webpack/components/SatelliteWelcomeEnv.js +54 -0
- data/webpack/components/fills/UpgradePage/SatelliteUpgradeHelperCard.js +11 -3
- data/webpack/documentation/documentationMenuKeyboard.js +89 -0
- data/webpack/documentation/documentationMenuKeyboard.test.js +229 -0
- data/webpack/documentation/hideDocumentation.js +173 -0
- data/webpack/documentation/hideDocumentation.test.js +294 -0
- data/webpack/index.js +34 -3
- data/webpack/test_setup.js +3 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f4492cd1ca5ab22e180b6767695d8a7d7d029915018c5214dd257dfbc1009c8
|
|
4
|
+
data.tar.gz: 593d0c39829ac5c5f2bd889c23baa151d0e824da8257510eea8238bf935f833b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e4089ace8320b932eb4e1a49dca2ee34e61125284a2e996b54f6621568337f9e863342350cc31b5caaf09c531b7ff0398e07ca3ee1a5d8330eb8ea2c02c31fe
|
|
7
|
+
data.tar.gz: 1fdfde63ecab76fc18516aaf11fab2d6808426b5594526caaa8c9313ba8292dfaccc0137db517892b6445220dc0141fc3839b8632673ff71167b95a87eda93ca
|
data/README.md
CHANGED
|
@@ -73,11 +73,18 @@ that will be branded automatically to
|
|
|
73
73
|
Many times we need to use a downstream link instead of an upstream one. The most
|
|
74
74
|
common example for this is pointing to a documentation link.
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
`
|
|
78
|
-
|
|
79
|
-
`
|
|
80
|
-
|
|
76
|
+
Documentation controls are hidden by this theme. Inline documentation links remain
|
|
77
|
+
visible, and the `manual`, `docs`, `plugin_manual`, `wiki`, and upgrade documentation
|
|
78
|
+
routes redirect to the configured Satellite version's landing page:
|
|
79
|
+
`{base}/en/documentation/red_hat_satellite/{version}`. The base comes from the
|
|
80
|
+
`satellite_documentation_url` setting, including offline documentation servers.
|
|
81
|
+
Direct Satellite documentation links in the page are rewritten to `/links/manual`
|
|
82
|
+
(with the application's URL prefix). Support and the upgrade helper keep their
|
|
83
|
+
existing destinations.
|
|
84
|
+
|
|
85
|
+
The detailed mappings in `lib/foreman_theme_satellite/documentation.rb` are retained
|
|
86
|
+
for future restoration, but these redirects currently bypass them. The mapping
|
|
87
|
+
examples below describe that retained mechanism, not the active redirect behavior.
|
|
81
88
|
|
|
82
89
|
In foreman:
|
|
83
90
|
``` ruby
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Hide identified standalone controls only. Do not match hrefs: welcome
|
|
2
|
+
// pages use those same /links destinations inside sentences.
|
|
3
|
+
.satellite-documentation-hidden,
|
|
4
|
+
.docs-element-to-remove,
|
|
5
|
+
[data-ouia-component-id="doc-url-dropdown"],
|
|
6
|
+
[data-ouia-component-id="action-buttons-documentation"],
|
|
7
|
+
[data-ouia-component-id="register-host-documentation-button"],
|
|
8
|
+
[data-ouia-component-id="audits-documentation-button"],
|
|
9
|
+
[data-ouia-component-id="inventory-documentation-button"],
|
|
10
|
+
[data-ouia-component-id="upgrade-docs-button"] {
|
|
11
|
+
display: none !important;
|
|
12
|
+
}
|
|
@@ -3,6 +3,19 @@ require_relative '../../../lib/foreman_theme_satellite/documentation'
|
|
|
3
3
|
module DocumentationControllerBranding
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
6
|
+
# Keep the detailed mappings below for future restoration. All public
|
|
7
|
+
# documentation redirects currently land on the configured version's index.
|
|
8
|
+
def external_url(type:, options: {})
|
|
9
|
+
documentation_redirect = %w[manual docs plugin_manual wiki].include?(type.to_s)
|
|
10
|
+
upgrade_documentation_redirect = type.to_s == 'upgrade' && options[:section] == 'documentation'
|
|
11
|
+
|
|
12
|
+
if documentation_redirect || upgrade_documentation_redirect
|
|
13
|
+
documentation_root_url
|
|
14
|
+
else
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
6
19
|
def documentation_url(section = nil, options = {})
|
|
7
20
|
url = ''
|
|
8
21
|
unless section.nil?
|
|
@@ -55,12 +68,10 @@ module DocumentationControllerBranding
|
|
|
55
68
|
"#{unversioned_documentation_root}/#{ForemanThemeSatellite.documentation_version}"
|
|
56
69
|
end
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
"#{unversioned_documentation_root}/#{ForemanThemeSatellite.documentation_version}/html-single"
|
|
60
|
-
end
|
|
71
|
+
alias_method :documentation_root, :documentation_root_url
|
|
61
72
|
|
|
62
73
|
def unversioned_documentation_root
|
|
63
|
-
"#{Setting[:satellite_documentation_url]}/documentation/
|
|
74
|
+
"#{Setting[:satellite_documentation_url]}/en/documentation/red_hat_satellite"
|
|
64
75
|
end
|
|
65
76
|
|
|
66
77
|
private
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Hide Rails documentation controls without changing their URL helpers or redirects.
|
|
2
|
+
module DocumentationVisibilityHelper
|
|
3
|
+
DOCUMENTATION_URL_TO_REMOVE = '#satellite-documentation-link-to-remove'.freeze
|
|
4
|
+
DOCUMENTATION_ELEMENT_TO_REMOVE_CLASS = 'docs-element-to-remove'.freeze
|
|
5
|
+
DOCUMENTATION_SECTIONS_TO_REMOVE = %w[
|
|
6
|
+
4.1.1LDAPAuthentication
|
|
7
|
+
5.7ExternalAuthentication
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
def help_button
|
|
11
|
+
''
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def documentation_button(*_args, **_options)
|
|
15
|
+
''
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def documentation_url(section = nil, *args, **options)
|
|
19
|
+
return DOCUMENTATION_URL_TO_REMOVE if DOCUMENTATION_SECTIONS_TO_REMOVE.include?(section.to_s)
|
|
20
|
+
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def link_to(name = nil, options = nil, html_options = nil, &block)
|
|
25
|
+
link_options = block_given? ? name : options
|
|
26
|
+
return tag.div(class: DOCUMENTATION_ELEMENT_TO_REMOVE_CLASS) if link_options == DOCUMENTATION_URL_TO_REMOVE
|
|
27
|
+
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Deface::Override.new(
|
|
2
|
+
virtual_path: 'foreman_puppet/config_groups/welcome',
|
|
3
|
+
name: 'satellite_hide_config_groups_documentation',
|
|
4
|
+
replace: "erb[loud]:contains('EmptyState')",
|
|
5
|
+
text: <<~ERB
|
|
6
|
+
<%= react_component('EmptyState', {
|
|
7
|
+
:icon => 'object-group',
|
|
8
|
+
:iconType => 'fa',
|
|
9
|
+
:header => _('Config Groups'),
|
|
10
|
+
:description => description,
|
|
11
|
+
:action => action
|
|
12
|
+
}) %>
|
|
13
|
+
ERB
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
Deface::Override.new(
|
|
17
|
+
virtual_path: 'foreman_puppet/environments/welcome',
|
|
18
|
+
name: 'satellite_hide_environments_documentation',
|
|
19
|
+
replace: "erb[loud]:contains('WelcomeEnv')",
|
|
20
|
+
text: <<~ERB
|
|
21
|
+
<%= react_component('SatelliteWelcomeEnv', {canCreate: authorized_for(action: :new)}) %>
|
|
22
|
+
ERB
|
|
23
|
+
)
|
|
@@ -102,6 +102,7 @@ module ForemanThemeSatellite
|
|
|
102
102
|
|
|
103
103
|
UINotifications::StringParser.send :prepend, DeprecationNotification::StringParser
|
|
104
104
|
Notification.singleton_class.send :prepend, DeprecationNotification::Notification
|
|
105
|
+
ApplicationHelper.prepend DocumentationVisibilityHelper
|
|
105
106
|
LinksController.prepend DocumentationControllerBranding
|
|
106
107
|
ProvisioningTemplatesController.include ProvisioningTemplatesControllerBranding
|
|
107
108
|
ProvisioningTemplatesHelper.prepend ProvisioningTemplatesHelperBranding
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { FormattedMessage } from 'react-intl';
|
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
5
|
+
import { foremanUrl, getManualURL } from 'foremanReact/common/helpers';
|
|
6
|
+
import EmptyState from 'foremanReact/components/common/EmptyState';
|
|
7
|
+
|
|
8
|
+
export const SatelliteWelcomeEnv = ({ canCreate }) => {
|
|
9
|
+
const action = canCreate && {
|
|
10
|
+
title: __('Create Puppet Environment'),
|
|
11
|
+
url: foremanUrl('/foreman_puppet/environments/new'),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const description = (
|
|
15
|
+
<FormattedMessage
|
|
16
|
+
id="puppetenv-welcome"
|
|
17
|
+
defaultMessage={__(
|
|
18
|
+
'If you are planning to use Foreman as an external node classifier you should provide information about one or more environments.{newLine}This information is commonly imported from a pre-existing Puppet configuration by the use of the {puppetClassesLinkToDocs} and environment importer.'
|
|
19
|
+
)}
|
|
20
|
+
values={{
|
|
21
|
+
newLine: <br />,
|
|
22
|
+
puppetClassesLinkToDocs: (
|
|
23
|
+
<a
|
|
24
|
+
target="_blank"
|
|
25
|
+
href={getManualURL('4.2.2Classes')}
|
|
26
|
+
rel="noreferrer"
|
|
27
|
+
>
|
|
28
|
+
{__('Puppet classes')}
|
|
29
|
+
</a>
|
|
30
|
+
),
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<EmptyState
|
|
37
|
+
icon="th"
|
|
38
|
+
iconType="fa"
|
|
39
|
+
header={__('Puppet Environments')}
|
|
40
|
+
description={description}
|
|
41
|
+
action={action}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
SatelliteWelcomeEnv.propTypes = {
|
|
47
|
+
canCreate: PropTypes.bool,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
SatelliteWelcomeEnv.defaultProps = {
|
|
51
|
+
canCreate: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default SatelliteWelcomeEnv;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardBody,
|
|
6
|
+
CardTitle,
|
|
7
|
+
CardFooter,
|
|
8
|
+
Button,
|
|
9
|
+
} from '@patternfly/react-core';
|
|
4
10
|
import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
|
|
5
11
|
|
|
6
12
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
@@ -10,8 +16,10 @@ const SatelliteUpgradeHelperCard = () => (
|
|
|
10
16
|
<Card ouiaId="upgrade-docs-satellite-helper" isFlat>
|
|
11
17
|
<CardTitle component="h4">{__('Satellite upgrade helper')}</CardTitle>
|
|
12
18
|
<CardBody>
|
|
13
|
-
{__(
|
|
14
|
-
|
|
19
|
+
{__(
|
|
20
|
+
'Generate a customized upgrade plan based on your current Satellite environment.' +
|
|
21
|
+
' This tool will guide you through the upgrade process and provide additional steps to avoid known issues specific to your upgrade scenario.'
|
|
22
|
+
)}
|
|
15
23
|
</CardBody>
|
|
16
24
|
<CardFooter>
|
|
17
25
|
<Button
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// PatternFly's legacy dropdown keeps references to CSS-hidden items. Handle
|
|
2
|
+
// navigation in affected menus before its handler attempts to focus those refs.
|
|
3
|
+
const navigationKeys = ['ArrowDown', 'ArrowUp', 'Home', 'End'];
|
|
4
|
+
const openingKeys = [...navigationKeys, 'Enter', ' '];
|
|
5
|
+
const dropdownSelector = '.pf-v5-c-dropdown';
|
|
6
|
+
const menuSelector = '.pf-v5-c-dropdown__menu';
|
|
7
|
+
const itemSelector = '[role="menuitem"]';
|
|
8
|
+
|
|
9
|
+
const isVisible = node => {
|
|
10
|
+
const { defaultView } = node.ownerDocument;
|
|
11
|
+
for (let ancestor = node; ancestor; ancestor = ancestor.parentElement) {
|
|
12
|
+
const style = defaultView.getComputedStyle(ancestor);
|
|
13
|
+
if (
|
|
14
|
+
ancestor.hidden ||
|
|
15
|
+
style.display === 'none' ||
|
|
16
|
+
style.visibility === 'hidden'
|
|
17
|
+
) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default function installMenuKeyboard(doc, isDocumentationControl) {
|
|
25
|
+
const frames = new Set();
|
|
26
|
+
const navigate = (dropdown, key, current) => {
|
|
27
|
+
const menu = dropdown.querySelector(menuSelector);
|
|
28
|
+
if (!menu || !isVisible(menu)) return false;
|
|
29
|
+
const items = [...menu.querySelectorAll(itemSelector)];
|
|
30
|
+
if (!items.some(isDocumentationControl)) return false;
|
|
31
|
+
const available = items.filter(
|
|
32
|
+
item =>
|
|
33
|
+
!isDocumentationControl(item) &&
|
|
34
|
+
!item.hasAttribute('disabled') &&
|
|
35
|
+
item.getAttribute('aria-disabled') !== 'true' &&
|
|
36
|
+
isVisible(item)
|
|
37
|
+
);
|
|
38
|
+
const lastIndex = available.length - 1;
|
|
39
|
+
const currentIndex = available.indexOf(current);
|
|
40
|
+
let nextIndex = 0;
|
|
41
|
+
if (key === 'End' || (key === 'ArrowUp' && currentIndex <= 0)) {
|
|
42
|
+
nextIndex = lastIndex;
|
|
43
|
+
} else if (key === 'ArrowUp') {
|
|
44
|
+
nextIndex = currentIndex - 1;
|
|
45
|
+
} else if (key === 'ArrowDown' && currentIndex >= 0) {
|
|
46
|
+
nextIndex = (currentIndex + 1) % available.length;
|
|
47
|
+
}
|
|
48
|
+
const target =
|
|
49
|
+
available[nextIndex] || dropdown.querySelector('[aria-expanded]');
|
|
50
|
+
if (target) target.focus();
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleEvent = event => {
|
|
55
|
+
const dropdown = event.target.closest(dropdownSelector);
|
|
56
|
+
if (!dropdown) return;
|
|
57
|
+
const toggle = event.target.closest('[aria-expanded]');
|
|
58
|
+
const key = event.type === 'click' ? 'Enter' : event.key;
|
|
59
|
+
if (event.type === 'keydown' && navigationKeys.includes(key)) {
|
|
60
|
+
const current = event.target.closest(itemSelector);
|
|
61
|
+
if (navigate(dropdown, key, current)) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
event.stopImmediatePropagation();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!toggle || !openingKeys.includes(key)) return;
|
|
68
|
+
// Let React open the menu and PatternFly finish its initial focus first.
|
|
69
|
+
// This also handles menus whose first item is hidden documentation.
|
|
70
|
+
const frame = doc.defaultView.requestAnimationFrame(() => {
|
|
71
|
+
frames.delete(frame);
|
|
72
|
+
if (
|
|
73
|
+
toggle.isConnected &&
|
|
74
|
+
toggle.getAttribute('aria-expanded') === 'true'
|
|
75
|
+
) {
|
|
76
|
+
navigate(dropdown, key, null);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
frames.add(frame);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
doc.addEventListener('keydown', handleEvent, true);
|
|
83
|
+
doc.addEventListener('click', handleEvent, true);
|
|
84
|
+
return () => {
|
|
85
|
+
doc.removeEventListener('keydown', handleEvent, true);
|
|
86
|
+
doc.removeEventListener('click', handleEvent, true);
|
|
87
|
+
frames.forEach(frame => doc.defaultView.cancelAnimationFrame(frame));
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
render,
|
|
6
|
+
fireEvent,
|
|
7
|
+
screen,
|
|
8
|
+
waitFor,
|
|
9
|
+
cleanup,
|
|
10
|
+
} from '@testing-library/react';
|
|
11
|
+
import '@testing-library/jest-dom';
|
|
12
|
+
import {
|
|
13
|
+
Dropdown,
|
|
14
|
+
DropdownItem,
|
|
15
|
+
DropdownToggle,
|
|
16
|
+
DropdownGroup,
|
|
17
|
+
} from '@patternfly/react-core/deprecated';
|
|
18
|
+
import { ActionButtons } from 'foremanReact/components/PF4/TableIndexPage/ActionButtons';
|
|
19
|
+
import DocumentationLink from 'foremanReact/components/PF4/DocumentationLink';
|
|
20
|
+
import { installDocumentationHiding } from './hideDocumentation';
|
|
21
|
+
|
|
22
|
+
const styles = fs
|
|
23
|
+
.readFileSync(
|
|
24
|
+
path.join(
|
|
25
|
+
__dirname,
|
|
26
|
+
'../../app/assets/stylesheets/foreman_theme_satellite/documentation.scss'
|
|
27
|
+
),
|
|
28
|
+
'utf8'
|
|
29
|
+
)
|
|
30
|
+
.replace(/^\/\/.*$/gm, '');
|
|
31
|
+
let installation;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
const style = document.createElement('style');
|
|
35
|
+
style.textContent = styles;
|
|
36
|
+
style.id = 'documentation-test-style';
|
|
37
|
+
document.head.appendChild(style);
|
|
38
|
+
installation = installDocumentationHiding(document);
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
installation.disconnect();
|
|
42
|
+
cleanup();
|
|
43
|
+
document.getElementById('documentation-test-style').remove();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Use the same grouped PatternFly menu and documentation component as bookmarks.
|
|
47
|
+
const BookmarksMenu = () => {
|
|
48
|
+
const [isOpen, setOpen] = useState(false);
|
|
49
|
+
return (
|
|
50
|
+
<Dropdown
|
|
51
|
+
ouiaId="test-bookmarks"
|
|
52
|
+
isOpen={isOpen}
|
|
53
|
+
isGrouped
|
|
54
|
+
toggle={
|
|
55
|
+
<DropdownToggle onToggle={(_event, open) => setOpen(open)}>
|
|
56
|
+
Bookmarks
|
|
57
|
+
</DropdownToggle>
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
<DropdownGroup>
|
|
61
|
+
<DocumentationLink href="/links/manual" />
|
|
62
|
+
<DropdownItem ouiaId="saved-bookmark" href="/hosts?search=saved">
|
|
63
|
+
Saved bookmark
|
|
64
|
+
</DropdownItem>
|
|
65
|
+
<DropdownItem ouiaId="disabled-bookmark" isDisabled>
|
|
66
|
+
Unavailable bookmark
|
|
67
|
+
</DropdownItem>
|
|
68
|
+
<DropdownItem ouiaId="manage-bookmarks">Manage bookmarks</DropdownItem>
|
|
69
|
+
</DropdownGroup>
|
|
70
|
+
</Dropdown>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const press = (element, key) =>
|
|
75
|
+
fireEvent.keyDown(element, {
|
|
76
|
+
key,
|
|
77
|
+
keyCode: {
|
|
78
|
+
ArrowDown: 40,
|
|
79
|
+
ArrowUp: 38,
|
|
80
|
+
Home: 36,
|
|
81
|
+
End: 35,
|
|
82
|
+
Enter: 13,
|
|
83
|
+
Escape: 27,
|
|
84
|
+
}[key],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('table dropdown skips documentation and wraps between functional actions', async () => {
|
|
88
|
+
const refresh = jest.fn();
|
|
89
|
+
render(
|
|
90
|
+
<ActionButtons
|
|
91
|
+
buttons={[
|
|
92
|
+
{ title: 'Create', action: { href: '/hosts/new' } },
|
|
93
|
+
{ title: 'Export', action: { href: '/hosts.csv' } },
|
|
94
|
+
{ title: 'Documentation', action: { href: '/hosts/help' } },
|
|
95
|
+
{ title: 'Refresh', action: { onClick: refresh } },
|
|
96
|
+
]}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
const toggle = screen.getByRole('button', { name: 'toggle action dropdown' });
|
|
100
|
+
toggle.focus();
|
|
101
|
+
press(toggle, 'Enter');
|
|
102
|
+
const first = await screen.findByRole('menuitem', { name: 'Export' });
|
|
103
|
+
const last = screen.getByRole('menuitem', { name: 'Refresh' });
|
|
104
|
+
await waitFor(() => expect(first).toHaveFocus());
|
|
105
|
+
await waitFor(() =>
|
|
106
|
+
expect(
|
|
107
|
+
screen.queryByRole('menuitem', { name: 'Documentation' })
|
|
108
|
+
).not.toBeInTheDocument()
|
|
109
|
+
);
|
|
110
|
+
press(first, 'ArrowDown');
|
|
111
|
+
expect(last).toHaveFocus();
|
|
112
|
+
press(last, 'ArrowDown');
|
|
113
|
+
expect(first).toHaveFocus();
|
|
114
|
+
press(first, 'ArrowUp');
|
|
115
|
+
expect(last).toHaveFocus();
|
|
116
|
+
press(last, 'Home');
|
|
117
|
+
expect(first).toHaveFocus();
|
|
118
|
+
press(first, 'End');
|
|
119
|
+
expect(last).toHaveFocus();
|
|
120
|
+
press(last, 'Enter');
|
|
121
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
122
|
+
press(last, 'Escape');
|
|
123
|
+
await waitFor(() => expect(toggle).toHaveFocus());
|
|
124
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test.each(['Enter', ' '])(
|
|
128
|
+
'opening with %s skips a hidden first item',
|
|
129
|
+
async (key) => {
|
|
130
|
+
render(<BookmarksMenu />);
|
|
131
|
+
const toggle = screen.getByRole('button', { name: 'Bookmarks' });
|
|
132
|
+
toggle.focus();
|
|
133
|
+
press(toggle, key);
|
|
134
|
+
const first = await screen.findByRole('menuitem', {
|
|
135
|
+
name: 'Saved bookmark',
|
|
136
|
+
});
|
|
137
|
+
await waitFor(() => expect(first).toHaveFocus());
|
|
138
|
+
expect(
|
|
139
|
+
screen.queryByRole('menuitem', { name: 'Documentation' })
|
|
140
|
+
).not.toBeInTheDocument();
|
|
141
|
+
press(first, 'ArrowDown');
|
|
142
|
+
expect(
|
|
143
|
+
screen.getByRole('menuitem', { name: 'Manage bookmarks' })
|
|
144
|
+
).toHaveFocus();
|
|
145
|
+
press(document.activeElement, 'ArrowDown');
|
|
146
|
+
expect(first).toHaveFocus();
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
test('ArrowUp from the open toggle focuses the last available action', async () => {
|
|
151
|
+
render(<BookmarksMenu />);
|
|
152
|
+
const toggle = screen.getByRole('button', { name: 'Bookmarks' });
|
|
153
|
+
toggle.focus();
|
|
154
|
+
fireEvent.click(toggle);
|
|
155
|
+
await waitFor(() =>
|
|
156
|
+
expect(
|
|
157
|
+
screen.getByRole('menuitem', { name: 'Saved bookmark' })
|
|
158
|
+
).toHaveFocus()
|
|
159
|
+
);
|
|
160
|
+
toggle.focus();
|
|
161
|
+
press(toggle, 'ArrowUp');
|
|
162
|
+
const last = await screen.findByRole('menuitem', {
|
|
163
|
+
name: 'Manage bookmarks',
|
|
164
|
+
});
|
|
165
|
+
await waitFor(() => expect(last).toHaveFocus());
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('mouse opening also avoids a hidden initial focus target', async () => {
|
|
169
|
+
render(<BookmarksMenu />);
|
|
170
|
+
fireEvent.click(screen.getByRole('button', { name: 'Bookmarks' }));
|
|
171
|
+
await waitFor(() =>
|
|
172
|
+
expect(
|
|
173
|
+
screen.getByRole('menuitem', { name: 'Saved bookmark' })
|
|
174
|
+
).toHaveFocus()
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('menus without documentation retain native keyboard handling', async () => {
|
|
179
|
+
render(
|
|
180
|
+
<ActionButtons
|
|
181
|
+
buttons={[
|
|
182
|
+
{ title: 'Create', action: {} },
|
|
183
|
+
{ title: 'Export', action: { href: '/hosts.csv' } },
|
|
184
|
+
{ title: 'Refresh', action: {} },
|
|
185
|
+
]}
|
|
186
|
+
/>
|
|
187
|
+
);
|
|
188
|
+
const toggle = screen.getByRole('button', { name: 'toggle action dropdown' });
|
|
189
|
+
fireEvent.click(toggle);
|
|
190
|
+
const first = await screen.findByRole('menuitem', { name: 'Export' });
|
|
191
|
+
first.focus();
|
|
192
|
+
press(first, 'ArrowDown');
|
|
193
|
+
expect(screen.getByRole('menuitem', { name: 'Refresh' })).toHaveFocus();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('disconnect removes the theme keyboard listener', async () => {
|
|
197
|
+
render(<BookmarksMenu />);
|
|
198
|
+
fireEvent.click(screen.getByRole('button', { name: 'Bookmarks' }));
|
|
199
|
+
const first = await screen.findByRole('menuitem', { name: 'Saved bookmark' });
|
|
200
|
+
await waitFor(() => expect(first).toHaveFocus());
|
|
201
|
+
installation.disconnect();
|
|
202
|
+
const bubble = jest.fn();
|
|
203
|
+
document.addEventListener('keydown', bubble);
|
|
204
|
+
press(first, 'Home');
|
|
205
|
+
expect(bubble).toHaveBeenCalled();
|
|
206
|
+
document.removeEventListener('keydown', bubble);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('a documentation-only menu keeps focus on its toggle and can close', async () => {
|
|
210
|
+
render(
|
|
211
|
+
<ActionButtons
|
|
212
|
+
buttons={[
|
|
213
|
+
{ title: 'Create', action: {} },
|
|
214
|
+
{ title: 'Documentation', action: { href: '/hosts/help' } },
|
|
215
|
+
]}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
const toggle = screen.getByRole('button', { name: 'toggle action dropdown' });
|
|
219
|
+
toggle.focus();
|
|
220
|
+
press(toggle, 'Enter');
|
|
221
|
+
await waitFor(() => expect(toggle).toHaveAttribute('aria-expanded', 'true'));
|
|
222
|
+
await waitFor(() =>
|
|
223
|
+
expect(screen.queryByRole('menuitem')).not.toBeInTheDocument()
|
|
224
|
+
);
|
|
225
|
+
press(toggle, 'ArrowDown');
|
|
226
|
+
expect(toggle).toHaveFocus();
|
|
227
|
+
press(toggle, 'Escape');
|
|
228
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
229
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import installMenuKeyboard from './documentationMenuKeyboard';
|
|
2
|
+
|
|
3
|
+
// Hide only identified documentation buttons and menu actions. Leave inline
|
|
4
|
+
// links and their surrounding text intact; never remove nodes owned by React.
|
|
5
|
+
export const hiddenClass = 'satellite-documentation-hidden';
|
|
6
|
+
|
|
7
|
+
const controlIds = [
|
|
8
|
+
'doc-url-dropdown',
|
|
9
|
+
'action-buttons-documentation',
|
|
10
|
+
'register-host-documentation-button',
|
|
11
|
+
'audits-documentation-button',
|
|
12
|
+
'inventory-documentation-button',
|
|
13
|
+
'upgrade-docs-button',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const controlIdSet = new Set(controlIds);
|
|
17
|
+
|
|
18
|
+
const controlSelector = controlIds
|
|
19
|
+
.map(id => `[data-ouia-component-id="${id}"]`)
|
|
20
|
+
.join(',');
|
|
21
|
+
|
|
22
|
+
const dropdownItemSelector =
|
|
23
|
+
'[data-ouia-component-id="action-buttons-dropdown"] [data-ouia-component-id]';
|
|
24
|
+
|
|
25
|
+
const httpHref = /^https?:\/\//i;
|
|
26
|
+
|
|
27
|
+
const satelliteDocsPattern = /\/(?:[a-z]{2}(?:-[a-z]{2})?\/documentation|documentation\/[a-z]{2}(?:-[a-z]{2})?)\/red_hat_satellite(?:\/|$)/i;
|
|
28
|
+
|
|
29
|
+
const isDocumentationControl = (node, documentationLabel) => {
|
|
30
|
+
const id = node.getAttribute('data-ouia-component-id');
|
|
31
|
+
if (!id) return false;
|
|
32
|
+
if (controlIdSet.has(id)) return true;
|
|
33
|
+
return (
|
|
34
|
+
id === `${documentationLabel}-dropdown-item` &&
|
|
35
|
+
node.closest('[data-ouia-component-id="action-buttons-dropdown"]') !== null
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const syncDocumentationControl = (node, documentationLabel) => {
|
|
40
|
+
if (!node || node.nodeType !== 1) return;
|
|
41
|
+
if (isDocumentationControl(node, documentationLabel)) {
|
|
42
|
+
node.classList.add(hiddenClass);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (node.classList.contains(hiddenClass)) node.classList.remove(hiddenClass);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const hideDocumentationIn = (root, documentationLabel) => {
|
|
49
|
+
if (!root || root.nodeType !== 1) return;
|
|
50
|
+
syncDocumentationControl(root, documentationLabel);
|
|
51
|
+
root.querySelectorAll(controlSelector).forEach(node => {
|
|
52
|
+
node.classList.add(hiddenClass);
|
|
53
|
+
});
|
|
54
|
+
const itemId = `${documentationLabel}-dropdown-item`;
|
|
55
|
+
root.querySelectorAll(dropdownItemSelector).forEach(node => {
|
|
56
|
+
if (node.getAttribute('data-ouia-component-id') === itemId) {
|
|
57
|
+
node.classList.add(hiddenClass);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const hideDocumentation = (
|
|
63
|
+
doc,
|
|
64
|
+
documentationLabel = 'Documentation'
|
|
65
|
+
) => {
|
|
66
|
+
// Recalculate so a React node reused for a functional link becomes visible.
|
|
67
|
+
doc
|
|
68
|
+
.querySelectorAll(`.${hiddenClass}`)
|
|
69
|
+
.forEach(node => node.classList.remove(hiddenClass));
|
|
70
|
+
hideDocumentationIn(doc.documentElement || doc, documentationLabel);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const normalizeLink = (link, landingPath) => {
|
|
74
|
+
const href = link.getAttribute('href');
|
|
75
|
+
// A rewrite sets href to the landing path and the observer hears that
|
|
76
|
+
// write. Matching it here makes the follow-up a no-op.
|
|
77
|
+
if (!href || href === landingPath || !httpHref.test(href)) return;
|
|
78
|
+
let url;
|
|
79
|
+
try {
|
|
80
|
+
url = new URL(href);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (satelliteDocsPattern.test(url.pathname)) {
|
|
85
|
+
link.setAttribute('href', landingPath);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const normalizeIn = (root, landingPath) => {
|
|
90
|
+
if (!root || root.nodeType !== 1) return;
|
|
91
|
+
if (typeof root.matches === 'function' && root.matches('a[href]')) {
|
|
92
|
+
normalizeLink(root, landingPath);
|
|
93
|
+
}
|
|
94
|
+
root.querySelectorAll('a[href]').forEach(link => {
|
|
95
|
+
normalizeLink(link, landingPath);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Direct Satellite documentation links bypass LinksController. Send them
|
|
100
|
+
// through its landing redirect too, including links in stored notifications.
|
|
101
|
+
export const normalizeSatelliteDocumentation = (doc, landingPath) => {
|
|
102
|
+
normalizeIn(doc.documentElement || doc, landingPath);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const installDocumentationHiding = (
|
|
106
|
+
doc,
|
|
107
|
+
documentationLabel = 'Documentation',
|
|
108
|
+
landingPath
|
|
109
|
+
) => {
|
|
110
|
+
hideDocumentation(doc, documentationLabel);
|
|
111
|
+
if (landingPath) normalizeSatelliteDocumentation(doc, landingPath);
|
|
112
|
+
|
|
113
|
+
const removeMenuKeyboard = installMenuKeyboard(doc, node =>
|
|
114
|
+
isDocumentationControl(node, documentationLabel)
|
|
115
|
+
);
|
|
116
|
+
const pending = [];
|
|
117
|
+
let scheduled = false;
|
|
118
|
+
const flush = () => {
|
|
119
|
+
scheduled = false;
|
|
120
|
+
const batch = pending.splice(0);
|
|
121
|
+
batch.forEach(change => {
|
|
122
|
+
if (change.type === 'childList') {
|
|
123
|
+
change.added.forEach(node => {
|
|
124
|
+
hideDocumentationIn(node, documentationLabel);
|
|
125
|
+
if (landingPath) normalizeIn(node, landingPath);
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (change.name === 'href') {
|
|
130
|
+
if (
|
|
131
|
+
landingPath &&
|
|
132
|
+
change.target.nodeType === 1 &&
|
|
133
|
+
change.target.matches('a[href]')
|
|
134
|
+
) {
|
|
135
|
+
normalizeLink(change.target, landingPath);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
syncDocumentationControl(change.target, documentationLabel);
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const observer = new doc.defaultView.MutationObserver(mutations => {
|
|
144
|
+
mutations.forEach(mutation => {
|
|
145
|
+
if (mutation.type === 'childList') {
|
|
146
|
+
if (mutation.addedNodes.length === 0) return;
|
|
147
|
+
pending.push({ type: 'childList', added: [...mutation.addedNodes] });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
pending.push({
|
|
151
|
+
type: 'attributes',
|
|
152
|
+
name: mutation.attributeName,
|
|
153
|
+
target: mutation.target,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
if (scheduled || pending.length === 0) return;
|
|
157
|
+
scheduled = true;
|
|
158
|
+
doc.defaultView.requestAnimationFrame(flush);
|
|
159
|
+
});
|
|
160
|
+
observer.observe(doc.body, {
|
|
161
|
+
subtree: true,
|
|
162
|
+
childList: true,
|
|
163
|
+
attributes: true,
|
|
164
|
+
attributeFilter: ['data-ouia-component-id', 'href'],
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
disconnect: () => {
|
|
168
|
+
observer.disconnect();
|
|
169
|
+
pending.length = 0;
|
|
170
|
+
removeMenuKeyboard();
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import assert from 'assert';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
hideDocumentation,
|
|
6
|
+
hiddenClass,
|
|
7
|
+
normalizeSatelliteDocumentation,
|
|
8
|
+
installDocumentationHiding,
|
|
9
|
+
} from './hideDocumentation';
|
|
10
|
+
|
|
11
|
+
const styles = fs
|
|
12
|
+
.readFileSync(
|
|
13
|
+
path.join(
|
|
14
|
+
__dirname,
|
|
15
|
+
'../../app/assets/stylesheets/foreman_theme_satellite/documentation.scss'
|
|
16
|
+
),
|
|
17
|
+
'utf8'
|
|
18
|
+
)
|
|
19
|
+
.replace(/^\/\/.*$/gm, '');
|
|
20
|
+
const createDOM = (html) => {
|
|
21
|
+
document.head.innerHTML = `<style>${styles}</style>`;
|
|
22
|
+
document.body.innerHTML = html;
|
|
23
|
+
};
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
document.head.innerHTML = '';
|
|
26
|
+
document.body.innerHTML = '';
|
|
27
|
+
jest.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('hides documentation controls but preserves inline links, sentences and actions', async () => {
|
|
31
|
+
createDOM(
|
|
32
|
+
`<div class="foreman-empty-state"><div><div class="empty-state-description">Description</div><span id="sentence">See <a href="/links/manual">documentation</a></span></div><ul id="permissions"><li>view_hosts</li></ul><a id="create" href="/hosts/new">Create</a></div><ul><li id="docs"><button data-ouia-component-id="doc-url-dropdown">Docs</button></li><li id="support"><a href="/links/support">Support</a></li></ul>`
|
|
33
|
+
);
|
|
34
|
+
const doc = document;
|
|
35
|
+
hideDocumentation(doc);
|
|
36
|
+
assert(doc.querySelector('#docs button').classList.contains(hiddenClass));
|
|
37
|
+
assert(!doc.getElementById('docs').classList.contains(hiddenClass));
|
|
38
|
+
['sentence', 'permissions', 'create', 'support'].forEach((id) => {
|
|
39
|
+
assert(!doc.getElementById(id).classList.contains(hiddenClass));
|
|
40
|
+
});
|
|
41
|
+
assert.equal(
|
|
42
|
+
doc.querySelector('#sentence a').getAttribute('href'),
|
|
43
|
+
'/links/manual'
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('handles React node reuse without leaving functional links hidden', async () => {
|
|
48
|
+
createDOM(
|
|
49
|
+
'<a data-ouia-component-id="audits-documentation-button" href="/links/manual">Documentation</a>'
|
|
50
|
+
);
|
|
51
|
+
const doc = document;
|
|
52
|
+
hideDocumentation(doc);
|
|
53
|
+
const link = doc.querySelector('a');
|
|
54
|
+
assert(link.classList.contains(hiddenClass));
|
|
55
|
+
link.setAttribute('data-ouia-component-id', 'create-host');
|
|
56
|
+
link.href = '/hosts/new';
|
|
57
|
+
hideDocumentation(doc);
|
|
58
|
+
assert(!link.classList.contains(hiddenClass));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('observes late documentation buttons and reused control identifiers', async () => {
|
|
62
|
+
createDOM('<main></main>');
|
|
63
|
+
const doc = document;
|
|
64
|
+
const observer = installDocumentationHiding(doc);
|
|
65
|
+
const link = doc.createElement('a');
|
|
66
|
+
link.href = '/links/docs/Managing_Hosts';
|
|
67
|
+
link.setAttribute(
|
|
68
|
+
'data-ouia-component-id',
|
|
69
|
+
'register-host-documentation-button'
|
|
70
|
+
);
|
|
71
|
+
doc.body.appendChild(link);
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
73
|
+
assert(link.classList.contains(hiddenClass));
|
|
74
|
+
link.setAttribute('data-ouia-component-id', 'create-host');
|
|
75
|
+
link.href = '/hosts/new';
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
77
|
+
assert(!link.classList.contains(hiddenClass));
|
|
78
|
+
observer.disconnect();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('hides localized table documentation without hiding other dropdown actions', async () => {
|
|
82
|
+
createDOM(
|
|
83
|
+
'<div data-ouia-component-id="action-buttons-dropdown"><ul><li id="docs"><a data-ouia-component-id="Dokumentace-dropdown-item" href="/hosts/help">Dokumentace</a></li><li id="export"><a data-ouia-component-id="Export-dropdown-item" href="/hosts.csv">Export</a></li></ul></div>'
|
|
84
|
+
);
|
|
85
|
+
hideDocumentation(document, 'Dokumentace');
|
|
86
|
+
assert(document.querySelector('#docs a').classList.contains(hiddenClass));
|
|
87
|
+
assert(!document.getElementById('export').classList.contains(hiddenClass));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('welcome sentences keep their documentation links', async () => {
|
|
91
|
+
assert.doesNotMatch(styles, /a\[href/);
|
|
92
|
+
createDOM(`
|
|
93
|
+
<div class="pf-v5-c-empty-state__body">
|
|
94
|
+
<div class="empty-state-description">No discovered hosts found in this context.</div>
|
|
95
|
+
<span id="discovered">For more information please see <a href="/links/plugin_manual?name=foreman_discovery">documentation</a></span>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="empty-state-description" id="reports">If you wish to configure Puppet to forward its reports to Foreman, please follow <a href="/links/manual/3.5.4PuppetReports">setting up reporting</a> and <a href="/links/wiki/Mail_Notifications">e-mail reporting</a></div>
|
|
98
|
+
`);
|
|
99
|
+
const doc = document;
|
|
100
|
+
hideDocumentation(doc, 'Documentation');
|
|
101
|
+
normalizeSatelliteDocumentation(doc, '/links/manual');
|
|
102
|
+
assert.equal(
|
|
103
|
+
doc.querySelector('#discovered').textContent,
|
|
104
|
+
'For more information please see documentation'
|
|
105
|
+
);
|
|
106
|
+
assert.equal(
|
|
107
|
+
doc.querySelector('#reports').textContent,
|
|
108
|
+
'If you wish to configure Puppet to forward its reports to Foreman, please follow setting up reporting and e-mail reporting'
|
|
109
|
+
);
|
|
110
|
+
doc.querySelectorAll('#discovered a, #reports a').forEach((link) => {
|
|
111
|
+
assert.notEqual(window.getComputedStyle(link).display, 'none');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('CSS and JavaScript leave inline documentation links visible', async () => {
|
|
116
|
+
const urls = [
|
|
117
|
+
'/links/manual',
|
|
118
|
+
'/links/docs/Managing_Hosts',
|
|
119
|
+
'/links/plugin_manual',
|
|
120
|
+
'/links/wiki',
|
|
121
|
+
'https://docs.redhat.com/en/documentation',
|
|
122
|
+
'https://docs.theforeman.org/',
|
|
123
|
+
];
|
|
124
|
+
createDOM(
|
|
125
|
+
urls.map((url) => `<p>See <a href="${url}">documentation</a>.</p>`).join('')
|
|
126
|
+
);
|
|
127
|
+
hideDocumentation(document);
|
|
128
|
+
document.querySelectorAll('a').forEach((link) => {
|
|
129
|
+
assert.notEqual(window.getComputedStyle(link).display, 'none');
|
|
130
|
+
assert.notEqual(
|
|
131
|
+
window.getComputedStyle(link.parentElement).display,
|
|
132
|
+
'none'
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('hides only the upgrade documentation button and preserves card content and helper', async () => {
|
|
138
|
+
createDOM(
|
|
139
|
+
'<section id="card"><p id="description">Upgrade instructions</p><a data-ouia-component-id="upgrade-docs-button" href="/links/upgrade?section=documentation">Documentation</a><a id="helper" data-ouia-component-id="upgrade-helper-button" href="/links/upgrade?section=helper">Upgrade helper</a></section>'
|
|
140
|
+
);
|
|
141
|
+
hideDocumentation(document);
|
|
142
|
+
const style = (selector) =>
|
|
143
|
+
window.getComputedStyle(document.querySelector(selector)).display;
|
|
144
|
+
assert.equal(style('[data-ouia-component-id="upgrade-docs-button"]'), 'none');
|
|
145
|
+
['#card', '#description', '#helper'].forEach((selector) => {
|
|
146
|
+
assert.notEqual(style(selector), 'none');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('direct Satellite links use the landing redirect while other products and support remain unchanged', async () => {
|
|
151
|
+
const satelliteUrls = [
|
|
152
|
+
'https://docs.redhat.com/en/documentation/red_hat_satellite/6.19/managing_hosts/index#chapter',
|
|
153
|
+
'https://access.redhat.com/documentation/en-us/red_hat_satellite/6.10/html/guide/chapter',
|
|
154
|
+
'https://offline.example/docs/en/documentation/red_hat_satellite/6.19/guide',
|
|
155
|
+
];
|
|
156
|
+
const preserved = [
|
|
157
|
+
'https://docs.redhat.com/en/documentation/subscription_central/1/guide',
|
|
158
|
+
'https://docs.ansible.com/ansible/latest/guide',
|
|
159
|
+
'https://access.redhat.com/products/red-hat-satellite#get-support',
|
|
160
|
+
'https://access.redhat.com/labs/satelliteupgradehelper',
|
|
161
|
+
'https://access.redhat.com/solutions/satellite6-tasks#task',
|
|
162
|
+
'https://access.redhat.com/articles/1586183',
|
|
163
|
+
];
|
|
164
|
+
createDOM(
|
|
165
|
+
[...satelliteUrls, ...preserved]
|
|
166
|
+
.map((url) => `<a href="${url}">Documentation</a>`)
|
|
167
|
+
.join('')
|
|
168
|
+
);
|
|
169
|
+
normalizeSatelliteDocumentation(document, '/prefix/links/manual');
|
|
170
|
+
const links = [...document.querySelectorAll('a')];
|
|
171
|
+
links.slice(0, satelliteUrls.length).forEach((link) => {
|
|
172
|
+
assert.equal(link.getAttribute('href'), '/prefix/links/manual');
|
|
173
|
+
assert.notEqual(window.getComputedStyle(link).display, 'none');
|
|
174
|
+
});
|
|
175
|
+
links
|
|
176
|
+
.slice(satelliteUrls.length)
|
|
177
|
+
.forEach((link, i) => assert.equal(link.href, preserved[i]));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('normalizes late Satellite links and React href updates', async () => {
|
|
181
|
+
createDOM('<main></main>');
|
|
182
|
+
const observer = installDocumentationHiding(
|
|
183
|
+
document,
|
|
184
|
+
'Documentation',
|
|
185
|
+
'/prefix/links/manual'
|
|
186
|
+
);
|
|
187
|
+
const link = document.createElement('a');
|
|
188
|
+
link.href =
|
|
189
|
+
'https://docs.redhat.com/en/documentation/red_hat_satellite/6.19/guide';
|
|
190
|
+
document.body.appendChild(link);
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
192
|
+
assert.equal(link.getAttribute('href'), '/prefix/links/manual');
|
|
193
|
+
link.href =
|
|
194
|
+
'https://docs.redhat.com/en/documentation/red_hat_satellite/6.19/another-guide';
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
196
|
+
assert.equal(link.getAttribute('href'), '/prefix/links/manual');
|
|
197
|
+
observer.disconnect();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('observer passes visit added subtrees and changed attributes only', async () => {
|
|
201
|
+
createDOM(
|
|
202
|
+
'<a id="outside" href="https://docs.ansible.com/ansible/latest/guide">Ansible</a><button id="kept" data-ouia-component-id="doc-url-dropdown">Docs</button><main></main>'
|
|
203
|
+
);
|
|
204
|
+
const doc = document;
|
|
205
|
+
const observer = installDocumentationHiding(
|
|
206
|
+
doc,
|
|
207
|
+
'Dokumentace',
|
|
208
|
+
'/prefix/links/manual'
|
|
209
|
+
);
|
|
210
|
+
const documentQueries = [];
|
|
211
|
+
const querySelectorAll = doc.querySelectorAll.bind(doc);
|
|
212
|
+
jest.spyOn(doc, 'querySelectorAll').mockImplementation((selector) => {
|
|
213
|
+
documentQueries.push(selector);
|
|
214
|
+
return querySelectorAll(selector);
|
|
215
|
+
});
|
|
216
|
+
const wrapper = doc.createElement('div');
|
|
217
|
+
wrapper.innerHTML =
|
|
218
|
+
'<div data-ouia-component-id="action-buttons-dropdown"><a id="localized" data-ouia-component-id="Dokumentace-dropdown-item" href="/hosts/help">Dokumentace</a></div><a id="inside" href="https://docs.redhat.com/en/documentation/red_hat_satellite/6.19/guide">Docs</a><button id="audits" data-ouia-component-id="audits-documentation-button">Documentation</button>';
|
|
219
|
+
doc.querySelector('main').appendChild(wrapper);
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
221
|
+
assert.equal(documentQueries.length, 0);
|
|
222
|
+
assert.equal(
|
|
223
|
+
doc.getElementById('inside').getAttribute('href'),
|
|
224
|
+
'/prefix/links/manual'
|
|
225
|
+
);
|
|
226
|
+
assert.equal(
|
|
227
|
+
doc.getElementById('outside').getAttribute('href'),
|
|
228
|
+
'https://docs.ansible.com/ansible/latest/guide'
|
|
229
|
+
);
|
|
230
|
+
assert(doc.getElementById('localized').classList.contains(hiddenClass));
|
|
231
|
+
assert(doc.getElementById('audits').classList.contains(hiddenClass));
|
|
232
|
+
assert(doc.getElementById('kept').classList.contains(hiddenClass));
|
|
233
|
+
doc
|
|
234
|
+
.getElementById('localized')
|
|
235
|
+
.setAttribute('data-ouia-component-id', 'Export-dropdown-item');
|
|
236
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
237
|
+
assert.equal(documentQueries.length, 0);
|
|
238
|
+
assert(!doc.getElementById('localized').classList.contains(hiddenClass));
|
|
239
|
+
assert(doc.getElementById('kept').classList.contains(hiddenClass));
|
|
240
|
+
assert(doc.getElementById('audits').classList.contains(hiddenClass));
|
|
241
|
+
observer.disconnect();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('observer applies a mutation that arrives while a frame is scheduled', async () => {
|
|
245
|
+
createDOM('<main></main>');
|
|
246
|
+
const doc = document;
|
|
247
|
+
const observer = installDocumentationHiding(
|
|
248
|
+
doc,
|
|
249
|
+
'Documentation',
|
|
250
|
+
'/prefix/links/manual'
|
|
251
|
+
);
|
|
252
|
+
let frame = null;
|
|
253
|
+
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
|
254
|
+
frame = callback;
|
|
255
|
+
return 1;
|
|
256
|
+
});
|
|
257
|
+
const link = doc.createElement('a');
|
|
258
|
+
link.href =
|
|
259
|
+
'https://docs.redhat.com/en/documentation/red_hat_satellite/6.19/guide';
|
|
260
|
+
doc.body.appendChild(link);
|
|
261
|
+
await Promise.resolve();
|
|
262
|
+
assert.equal(typeof frame, 'function');
|
|
263
|
+
const scheduledFrame = frame;
|
|
264
|
+
const button = doc.createElement('button');
|
|
265
|
+
button.setAttribute('data-ouia-component-id', 'audits-documentation-button');
|
|
266
|
+
doc.body.appendChild(button);
|
|
267
|
+
await Promise.resolve();
|
|
268
|
+
scheduledFrame();
|
|
269
|
+
assert.equal(link.getAttribute('href'), '/prefix/links/manual');
|
|
270
|
+
assert(button.classList.contains(hiddenClass));
|
|
271
|
+
observer.disconnect();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('Reports inline sentence stays complete even beside a hidden documentation button', async () => {
|
|
275
|
+
createDOM(
|
|
276
|
+
'<ul><li id="report"><div class="empty-state-description">If you wish to configure Puppet to forward its reports to Foreman, please follow <a href="/links/manual/3.5.4PuppetReports">setting up reporting</a> and <a href="/links/wiki/Mail_Notifications">e-mail reporting</a></div><button data-ouia-component-id="audits-documentation-button">Documentation</button></li></ul>'
|
|
277
|
+
);
|
|
278
|
+
const doc = document;
|
|
279
|
+
hideDocumentation(doc);
|
|
280
|
+
normalizeSatelliteDocumentation(doc, '/links/manual');
|
|
281
|
+
assert.equal(
|
|
282
|
+
doc.querySelector('.empty-state-description').textContent,
|
|
283
|
+
'If you wish to configure Puppet to forward its reports to Foreman, please follow setting up reporting and e-mail reporting'
|
|
284
|
+
);
|
|
285
|
+
doc.querySelectorAll('.empty-state-description a').forEach((link) => {
|
|
286
|
+
for (let node = link; node; node = node.parentElement) {
|
|
287
|
+
assert.notEqual(window.getComputedStyle(node).display, 'none');
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
assert.equal(
|
|
291
|
+
window.getComputedStyle(doc.querySelector('button')).display,
|
|
292
|
+
'none'
|
|
293
|
+
);
|
|
294
|
+
});
|
data/webpack/index.js
CHANGED
|
@@ -3,25 +3,56 @@ import React from 'react';
|
|
|
3
3
|
import componentRegistry from 'foremanReact/components/componentRegistry';
|
|
4
4
|
import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill';
|
|
5
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
6
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
6
7
|
import { Helmet } from 'react-helmet';
|
|
7
8
|
|
|
9
|
+
import { installDocumentationHiding } from './documentation/hideDocumentation';
|
|
10
|
+
|
|
8
11
|
import SatelliteUpgradeHelperCard from './components/fills/UpgradePage/SatelliteUpgradeHelperCard';
|
|
12
|
+
import { SatelliteWelcomeEnv } from './components/SatelliteWelcomeEnv';
|
|
13
|
+
|
|
14
|
+
const SATELLITE_FILL_WEIGHT = 1000;
|
|
9
15
|
|
|
10
16
|
// register components for erb mounting
|
|
11
17
|
componentRegistry.register({ name: 'Helmet', type: Helmet });
|
|
18
|
+
componentRegistry.register({
|
|
19
|
+
name: 'SatelliteWelcomeEnv',
|
|
20
|
+
type: SatelliteWelcomeEnv,
|
|
21
|
+
});
|
|
12
22
|
|
|
13
23
|
addGlobalFill(
|
|
14
24
|
'upgrade-page-upgrade-docs',
|
|
15
25
|
'Satellite upgrade helper card',
|
|
16
26
|
<SatelliteUpgradeHelperCard key="satellite-upgrade-helper-card" />,
|
|
17
|
-
|
|
27
|
+
SATELLITE_FILL_WEIGHT
|
|
18
28
|
);
|
|
19
29
|
addGlobalFill(
|
|
20
30
|
'upgrade-page-footer',
|
|
21
31
|
'Satellite upgrade page footer',
|
|
22
32
|
{
|
|
23
|
-
helpDesc: __(
|
|
33
|
+
helpDesc: __(
|
|
34
|
+
'If you require assistance or encounter issues, reach out to Red Hat Support or explore the resources available in the Customer Portal for troubleshooting.'
|
|
35
|
+
),
|
|
24
36
|
helpLinkText: __('Contact support'),
|
|
25
37
|
},
|
|
26
|
-
|
|
38
|
+
SATELLITE_FILL_WEIGHT
|
|
27
39
|
);
|
|
40
|
+
|
|
41
|
+
if (document.readyState === 'loading') {
|
|
42
|
+
document.addEventListener(
|
|
43
|
+
'DOMContentLoaded',
|
|
44
|
+
() =>
|
|
45
|
+
installDocumentationHiding(
|
|
46
|
+
document,
|
|
47
|
+
__('Documentation'),
|
|
48
|
+
foremanUrl('/links/manual')
|
|
49
|
+
),
|
|
50
|
+
{ once: true }
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
installDocumentationHiding(
|
|
54
|
+
document,
|
|
55
|
+
__('Documentation'),
|
|
56
|
+
foremanUrl('/links/manual')
|
|
57
|
+
);
|
|
58
|
+
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_theme_satellite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 16.4.
|
|
4
|
+
version: 16.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shimon Stein
|
|
@@ -66,6 +66,7 @@ files:
|
|
|
66
66
|
- app/assets/javascripts/foreman_theme_satellite/theme_ng_page_title.js
|
|
67
67
|
- app/assets/stylesheets/foreman_theme_satellite/application_colors.css
|
|
68
68
|
- app/assets/stylesheets/foreman_theme_satellite/bastion.scss
|
|
69
|
+
- app/assets/stylesheets/foreman_theme_satellite/documentation.scss
|
|
69
70
|
- app/assets/stylesheets/foreman_theme_satellite/external_logout.scss
|
|
70
71
|
- app/assets/stylesheets/foreman_theme_satellite/foreman_theme_satellite.css
|
|
71
72
|
- app/assets/stylesheets/foreman_theme_satellite/login.scss
|
|
@@ -76,6 +77,7 @@ files:
|
|
|
76
77
|
- app/assets/stylesheets/foreman_theme_satellite/wizard.scss
|
|
77
78
|
- app/controllers/concerns/documentation_controller_branding.rb
|
|
78
79
|
- app/controllers/concerns/provisioning_templates_controller_branding.rb
|
|
80
|
+
- app/helpers/documentation_visibility_helper.rb
|
|
79
81
|
- app/helpers/provisioning_templates_helper_branding.rb
|
|
80
82
|
- app/helpers/theme_application_helper.rb
|
|
81
83
|
- app/helpers/theme_css_class_helper.rb
|
|
@@ -94,6 +96,7 @@ files:
|
|
|
94
96
|
- app/models/concerns/setting_branding.rb
|
|
95
97
|
- app/models/concerns/upstream_only_settings.rb
|
|
96
98
|
- app/overrides/bootdisk_help_page.rb
|
|
99
|
+
- app/overrides/hide_documentation.rb
|
|
97
100
|
- app/overrides/layouts/base/eol_banner.html.erb.deface
|
|
98
101
|
- app/overrides/layouts/base/theme_react_root.html.erb.deface
|
|
99
102
|
- app/overrides/layouts/base/theme_resources.html.erb.deface
|
|
@@ -148,8 +151,14 @@ files:
|
|
|
148
151
|
- locale/zh_CN/LC_MESSAGES/foreman_theme_satellite.mo
|
|
149
152
|
- locale/zh_CN/foreman_theme_satellite.po
|
|
150
153
|
- package.json
|
|
154
|
+
- webpack/components/SatelliteWelcomeEnv.js
|
|
151
155
|
- webpack/components/fills/UpgradePage/SatelliteUpgradeHelperCard.js
|
|
156
|
+
- webpack/documentation/documentationMenuKeyboard.js
|
|
157
|
+
- webpack/documentation/documentationMenuKeyboard.test.js
|
|
158
|
+
- webpack/documentation/hideDocumentation.js
|
|
159
|
+
- webpack/documentation/hideDocumentation.test.js
|
|
152
160
|
- webpack/index.js
|
|
161
|
+
- webpack/test_setup.js
|
|
153
162
|
homepage: https://github.com/RedHatSatellite/foreman_theme_satellite
|
|
154
163
|
licenses:
|
|
155
164
|
- GPL-3.0
|