diff --git a/.eslintrc b/.eslintrc
index 206609861e0..992dfe343fa 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -105,6 +105,7 @@
"nonpersistent",
"noopener",
"noreferrer",
+ "nowrap",
"num",
"numpad",
"operatingsystem",
diff --git a/app/assets/stylesheets/base.scss b/app/assets/stylesheets/base.scss
index 35834c1662c..4cd4a168e08 100644
--- a/app/assets/stylesheets/base.scss
+++ b/app/assets/stylesheets/base.scss
@@ -5,14 +5,26 @@ html {
}
#rails-app-content {
+ --header-height: 70px;
position: absolute;
- top: 70px;
+ top: var(--header-height);
left: 0;
right: 0;
bottom: 0;
overflow: auto;
- height: calc(100% - 70px);
+ height: calc(100% - var(--header-height));
margin-left: 250px;
+ &.user-banner-present {
+ --banner-height: calc(
+ 2 * var(--pf-global--spacer--xs) +
+ (var(--pf-global--LineHeight--md) * var(--pf-global--FontSize--sm))
+ ); // banner height is line height and a small padding
+ top: calc(
+ var(--header-height) + var(--banner-height)
+ );
+
+ height: calc(100% - var(--header-height) - var(--banner-height));
+ }
.rails-table-toolbar {
padding-bottom: 0;
display: flex;
@@ -24,7 +36,6 @@ html {
grid-template-columns: unset;
grid-template-areas: unset;
}
-
}
body {
diff --git a/app/helpers/layout_helper.rb b/app/helpers/layout_helper.rb
index 8ca268df7ce..bdef520fc67 100644
--- a/app/helpers/layout_helper.rb
+++ b/app/helpers/layout_helper.rb
@@ -51,7 +51,8 @@ def layout_data
user: fetch_user, brand: 'foreman',
root: main_app.root_path,
locations: fetch_locations, orgs: fetch_organizations,
- instance_title: Setting[:instance_title]
+ instance_title: Setting[:instance_title],
+ instance_color: Setting[:instance_color]
}
end
diff --git a/app/registries/foreman/settings/general.rb b/app/registries/foreman/settings/general.rb
index bb2c86a2c40..bf62e7eed3e 100644
--- a/app/registries/foreman/settings/general.rb
+++ b/app/registries/foreman/settings/general.rb
@@ -86,9 +86,14 @@
collection: timezones)
setting('instance_title',
type: :string,
- description: N_("The instance title is shown on the top navigation bar (requires a page reload)."),
+ description: N_("The instance title is shown above the header in a banner (requires a page reload)."),
default: nil,
full_name: N_('Instance title'))
+ setting('instance_color',
+ type: :string,
+ description: N_("Hex value. for color for the instance title banner. Will only be used if an instance title is defined. If no valid value is given, the default value is: #000000. (requires a page reload)."),
+ default: '#000000',
+ full_name: N_('Instance color'))
setting('audits_period',
type: :integer,
description: N_('Duration in days to preserve audits for. Leave empty to disable the audits cleanup.'),
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb
index c1f0f205bd0..51f555a2946 100644
--- a/app/views/layouts/base.html.erb
+++ b/app/views/layouts/base.html.erb
@@ -59,7 +59,7 @@
<% end %>
<%= yield(:content) %>
diff --git a/webpack/assets/javascripts/react_app/components/Layout/Layout.js b/webpack/assets/javascripts/react_app/components/Layout/Layout.js
index cfa0c73d769..a740ad334a7 100644
--- a/webpack/assets/javascripts/react_app/components/Layout/Layout.js
+++ b/webpack/assets/javascripts/react_app/components/Layout/Layout.js
@@ -1,7 +1,8 @@
import React from 'react';
-import { Page, PageSidebar } from '@patternfly/react-core';
+import { Page, PageSidebar, Flex, FlexItem } from '@patternfly/react-core';
import { layoutPropTypes, layoutDefaultProps } from './LayoutHelper';
+import { InstanceBanner } from './components/InstanceBanner';
import Header from './components/Toolbar/Header';
import Navigation from './Navigation';
import './layout.scss';
@@ -29,28 +30,44 @@ const Layout = ({
};
return (
<>
-
- }
- id="foreman-page"
- sidebar={
-
+
+
+
+
+
}
- />
- }
- >
- {children}
-
+ id="foreman-page"
+ sidebar={
+
+ }
+ />
+ }
+ >
+ {children}
+
+
+
>
);
};
diff --git a/webpack/assets/javascripts/react_app/components/Layout/__tests__/Layout.test.js b/webpack/assets/javascripts/react_app/components/Layout/__tests__/Layout.test.js
index 780b2e6fd39..0f0ff7e619b 100644
--- a/webpack/assets/javascripts/react_app/components/Layout/__tests__/Layout.test.js
+++ b/webpack/assets/javascripts/react_app/components/Layout/__tests__/Layout.test.js
@@ -31,5 +31,6 @@ describe('Layout', () => {
expect(screen.getByText('Monitor')).toBeVisible();
expect(screen.getByText('Dashboard')).not.toBeVisible();
expect(screen.getByText('All Hosts')).toBeVisible();
+ expect(screen.getByText('Production')).toBeVisible();
});
});
diff --git a/webpack/assets/javascripts/react_app/components/Layout/components/InstanceBanner.js b/webpack/assets/javascripts/react_app/components/Layout/components/InstanceBanner.js
new file mode 100644
index 00000000000..6c759b0e1b0
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/Layout/components/InstanceBanner.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Banner } from '@patternfly/react-core';
+
+const getContrastColor = backgroundColor => {
+ const hexToRgb = hex => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result
+ ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16),
+ }
+ : null;
+ };
+ backgroundColor = hexToRgb(backgroundColor);
+ // Calculate the relative luminance of the background color
+ const luminance =
+ (0.2126 * backgroundColor.r +
+ 0.7152 * backgroundColor.g +
+ 0.0722 * backgroundColor.b) /
+ 255;
+
+ // Choose black or white text based on the relative luminance
+ return luminance > 0.5 ? 'black' : 'white';
+};
+const validateHexColor = instanceColor => {
+ // Check if the string is a valid hex color code
+ if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(instanceColor)) {
+ return instanceColor;
+ }
+ return '#000000';
+};
+
+export const InstanceBanner = ({ data }) => {
+ if (!data || !data.instance_title) {
+ return null;
+ }
+ const instance = data.instance_title;
+ const instanceColor = validateHexColor(data.instance_color);
+ return (
+ instance && (
+
+
+ {instance}
+
+
+ )
+ );
+};
+
+InstanceBanner.propTypes = {
+ data: PropTypes.shape({
+ instance_title: PropTypes.string,
+ instance_color: PropTypes.string,
+ }),
+};
+
+InstanceBanner.defaultProps = {
+ data: {},
+};
diff --git a/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/HeaderToolbar.js b/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/HeaderToolbar.js
index e0346ddc87b..0f1674cf025 100644
--- a/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/HeaderToolbar.js
+++ b/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/HeaderToolbar.js
@@ -17,7 +17,6 @@ import {
organizationPropType,
userPropType,
} from '../../LayoutHelper';
-import InstanceTitleViewer from './InstanceTitleViewer';
import './HeaderToolbar.scss';
const HeaderToolbar = ({
@@ -26,7 +25,6 @@ const HeaderToolbar = ({
notification_url: notificationUrl,
user,
stop_impersonation_url: stopImpersonationUrl,
- instance_title: instanceTitle,
isLoading,
}) => (
@@ -39,9 +37,6 @@ const HeaderToolbar = ({
/>
-
-
-
@@ -60,7 +55,6 @@ const HeaderToolbar = ({
);
HeaderToolbar.propTypes = {
stop_impersonation_url: PropTypes.string.isRequired,
- instance_title: PropTypes.string,
locations: locationPropType.isRequired,
orgs: organizationPropType.isRequired,
notification_url: PropTypes.string.isRequired,
@@ -69,7 +63,6 @@ HeaderToolbar.propTypes = {
};
HeaderToolbar.defaultProps = {
- instance_title: null,
user: {},
isLoading: layoutDefaultProps.isLoading,
};
diff --git a/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/InstanceTitleViewer.js b/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/InstanceTitleViewer.js
deleted file mode 100644
index cd6e89539b4..00000000000
--- a/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/InstanceTitleViewer.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react';
-import { Icon } from 'patternfly-react';
-import { Tooltip, TooltipPosition } from '@patternfly/react-core';
-import PropTypes from 'prop-types';
-
-const InstanceTitleViewer = ({ title }) => {
- if (!title) {
- return null;
- }
-
- return (
-
-
-
- );
-};
-
-InstanceTitleViewer.propTypes = {
- /** Title to display */
- title: PropTypes.string,
-};
-InstanceTitleViewer.defaultProps = {
- title: '',
-};
-export default InstanceTitleViewer;
diff --git a/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/__snapshots__/HeaderToolbar.test.js.snap b/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/__snapshots__/HeaderToolbar.test.js.snap
index 324a0762eaf..cba23cba620 100644
--- a/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/__snapshots__/HeaderToolbar.test.js.snap
+++ b/webpack/assets/javascripts/react_app/components/Layout/components/Toolbar/__snapshots__/HeaderToolbar.test.js.snap
@@ -53,11 +53,6 @@ exports[`HeaderToolbar rendering render HeaderToolbar 1`] = `
}
}
>
-
-
-