mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Theming: Support for runtime theme switching and hooks for custom themes (#31301)
* WIP Custom themes * Load custom themes from URL and via event * Dynamic page background * Header color change * Fixing tests and emotion warnings * Fixed test * moving cx to getStyles * Review fixes * minor change
This commit is contained in:
@@ -4,7 +4,7 @@ import { LinkButton } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
searchQuery: string;
|
||||
setSearchQuery: (value: string) => {};
|
||||
setSearchQuery: (value: string) => void;
|
||||
linkButton: { href: string; title: string };
|
||||
target?: string;
|
||||
}
|
||||
|
@@ -1,62 +1,58 @@
|
||||
// Libraries
|
||||
import React, { Component, HTMLAttributes } from 'react';
|
||||
import React, { FC, HTMLAttributes, useEffect } from 'react';
|
||||
import { getTitleFromNavModel } from 'app/core/selectors/navModel';
|
||||
|
||||
// Components
|
||||
import PageHeader from '../PageHeader/PageHeader';
|
||||
import { Footer } from '../Footer/Footer';
|
||||
import PageContents from './PageContents';
|
||||
import { CustomScrollbar } from '@grafana/ui';
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { isEqual } from 'lodash';
|
||||
import { PageContents } from './PageContents';
|
||||
import { CustomScrollbar, useStyles } from '@grafana/ui';
|
||||
import { GrafanaTheme, NavModel } from '@grafana/data';
|
||||
import { Branding } from '../Branding/Branding';
|
||||
import { css } from 'emotion';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
navModel: NavModel;
|
||||
}
|
||||
|
||||
class Page extends Component<Props> {
|
||||
static Header = PageHeader;
|
||||
static Contents = PageContents;
|
||||
|
||||
componentDidMount() {
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (!isEqual(prevProps.navModel, this.props.navModel)) {
|
||||
this.updateTitle();
|
||||
}
|
||||
}
|
||||
|
||||
updateTitle = () => {
|
||||
const title = this.getPageTitle;
|
||||
document.title = title ? title + ' - ' + Branding.AppTitle : Branding.AppTitle;
|
||||
};
|
||||
|
||||
get getPageTitle() {
|
||||
const { navModel } = this.props;
|
||||
if (navModel) {
|
||||
return getTitleFromNavModel(navModel) || undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { navModel, children, ...otherProps } = this.props;
|
||||
return (
|
||||
<div {...otherProps} className="page-scrollbar-wrapper">
|
||||
<CustomScrollbar autoHeightMin={'100%'}>
|
||||
<div className="page-scrollbar-content">
|
||||
<PageHeader model={navModel} />
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export interface PageType extends FC<Props> {
|
||||
Header: typeof PageHeader;
|
||||
Contents: typeof PageContents;
|
||||
}
|
||||
|
||||
export const Page: PageType = ({ navModel, children, ...otherProps }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
const title = getTitleFromNavModel(navModel);
|
||||
document.title = title ? `${title} - ${Branding.AppTitle}` : Branding.AppTitle;
|
||||
}, [navModel]);
|
||||
|
||||
return (
|
||||
<div {...otherProps} className={styles.wrapper}>
|
||||
<CustomScrollbar autoHeightMin={'100%'}>
|
||||
<div className="page-scrollbar-content">
|
||||
<PageHeader model={navModel} />
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Page.Header = PageHeader;
|
||||
Page.Contents = PageContents;
|
||||
|
||||
export default Page;
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
wrapper: css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: ${theme.colors.bg1};
|
||||
`,
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// Libraries
|
||||
import React, { Component } from 'react';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
// Components
|
||||
import PageLoader from '../PageLoader/PageLoader';
|
||||
@@ -9,12 +9,6 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
class PageContents extends Component<Props> {
|
||||
render() {
|
||||
const { isLoading } = this.props;
|
||||
|
||||
return <div className="page-container page-body">{isLoading ? <PageLoader /> : this.props.children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default PageContents;
|
||||
export const PageContents: FC<Props> = ({ isLoading, children }) => {
|
||||
return <div className="page-container page-body">{isLoading ? <PageLoader /> : children}</div>;
|
||||
};
|
||||
|
@@ -1,12 +1,10 @@
|
||||
import React from 'react';
|
||||
import PageHeader from './PageHeader';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
describe('PageHeader', () => {
|
||||
let wrapper: ShallowWrapper<PageHeader>;
|
||||
|
||||
describe('when the nav tree has a node with a title', () => {
|
||||
beforeAll(() => {
|
||||
it('should render the title', async () => {
|
||||
const nav = {
|
||||
main: {
|
||||
icon: 'folder-open',
|
||||
@@ -17,17 +15,15 @@ describe('PageHeader', () => {
|
||||
},
|
||||
node: {},
|
||||
};
|
||||
wrapper = shallow(<PageHeader model={nav as any} />);
|
||||
});
|
||||
|
||||
it('should render the title', () => {
|
||||
const title = wrapper.find('.page-header__title');
|
||||
expect(title.text()).toBe('node');
|
||||
render(<PageHeader model={nav as any} />);
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'node' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the nav tree has a node with breadcrumbs and a title', () => {
|
||||
beforeAll(() => {
|
||||
it('should render the title with breadcrumbs first and then title last', async () => {
|
||||
const nav = {
|
||||
main: {
|
||||
icon: 'folder-open',
|
||||
@@ -39,15 +35,11 @@ describe('PageHeader', () => {
|
||||
},
|
||||
node: {},
|
||||
};
|
||||
wrapper = shallow(<PageHeader model={nav as any} />);
|
||||
});
|
||||
|
||||
it('should render the title with breadcrumbs first and then title last', () => {
|
||||
const title = wrapper.find('.page-header__title');
|
||||
expect(title.text()).toBe('Parent / child');
|
||||
render(<PageHeader model={nav as any} />);
|
||||
|
||||
const parentLink = wrapper.find('.page-header__title > a.text-link');
|
||||
expect(parentLink.prop('href')).toBe('parentUrl');
|
||||
expect(screen.getByRole('heading', { name: 'Parent / child' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Parent' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { Tab, TabsBar, Icon, IconName } from '@grafana/ui';
|
||||
import { NavModel, NavModelItem, NavModelBreadcrumb } from '@grafana/data';
|
||||
import { Tab, TabsBar, Icon, IconName, useStyles } from '@grafana/ui';
|
||||
import { NavModel, NavModelItem, NavModelBreadcrumb, GrafanaTheme } from '@grafana/data';
|
||||
import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem';
|
||||
|
||||
export interface Props {
|
||||
@@ -71,86 +71,77 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default class PageHeader extends React.Component<Props, any> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
export const PageHeader: FC<Props> = ({ model }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
//Hack to re-render on changed props from angular with the @observer decorator
|
||||
return true;
|
||||
}
|
||||
const main = model.main;
|
||||
const children = main.children;
|
||||
|
||||
renderTitle(title: string, breadcrumbs: NavModelBreadcrumb[]) {
|
||||
if (!title && (!breadcrumbs || breadcrumbs.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!breadcrumbs || breadcrumbs.length === 0) {
|
||||
return <h1 className="page-header__title">{title}</h1>;
|
||||
}
|
||||
|
||||
const breadcrumbsResult = [];
|
||||
for (const bc of breadcrumbs) {
|
||||
if (bc.url) {
|
||||
breadcrumbsResult.push(
|
||||
<a className="text-link" key={breadcrumbsResult.length} href={bc.url}>
|
||||
{bc.title}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
breadcrumbsResult.push(<span key={breadcrumbsResult.length}> / {bc.title}</span>);
|
||||
}
|
||||
}
|
||||
breadcrumbsResult.push(<span key={breadcrumbs.length + 1}> / {title}</span>);
|
||||
|
||||
return <h1 className="page-header__title">{breadcrumbsResult}</h1>;
|
||||
}
|
||||
|
||||
renderHeaderTitle(main: NavModelItem) {
|
||||
const iconClassName =
|
||||
main.icon === 'grafana'
|
||||
? css`
|
||||
margin-top: 12px;
|
||||
`
|
||||
: css`
|
||||
margin-top: 14px;
|
||||
`;
|
||||
|
||||
return (
|
||||
<div className="page-header__inner">
|
||||
<span className="page-header__logo">
|
||||
{main.icon && <Icon name={main.icon as IconName} size="xxxl" className={iconClassName} />}
|
||||
{main.img && <img className="page-header__img" src={main.img} alt={`logo of ${main.text}`} />}
|
||||
</span>
|
||||
|
||||
<div className="page-header__info-block">
|
||||
{this.renderTitle(main.text, main.breadcrumbs ?? [])}
|
||||
{main.subTitle && <div className="page-header__sub-title">{main.subTitle}</div>}
|
||||
return (
|
||||
<div className={styles.headerCanvas}>
|
||||
<div className="page-container">
|
||||
<div className="page-header">
|
||||
{renderHeaderTitle(main)}
|
||||
{children && children.length && <Navigation>{children}</Navigation>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { model } = this.props;
|
||||
function renderHeaderTitle(main: NavModelItem) {
|
||||
const marginTop = main.icon === 'grafana' ? 12 : 14;
|
||||
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="page-header__inner">
|
||||
<span className="page-header__logo">
|
||||
{main.icon && <Icon name={main.icon as IconName} size="xxxl" style={{ marginTop }} />}
|
||||
{main.img && <img className="page-header__img" src={main.img} alt={`logo of ${main.text}`} />}
|
||||
</span>
|
||||
|
||||
const main = model.main;
|
||||
const children = main.children;
|
||||
|
||||
return (
|
||||
<div className="page-header-canvas">
|
||||
<div className="page-container">
|
||||
<div className="page-header">
|
||||
{this.renderHeaderTitle(main)}
|
||||
{children && children.length && <Navigation>{children}</Navigation>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-header__info-block">
|
||||
{renderTitle(main.text, main.breadcrumbs ?? [])}
|
||||
{main.subTitle && <div className="page-header__sub-title">{main.subTitle}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTitle(title: string, breadcrumbs: NavModelBreadcrumb[]) {
|
||||
if (!title && (!breadcrumbs || breadcrumbs.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!breadcrumbs || breadcrumbs.length === 0) {
|
||||
return <h1 className="page-header__title">{title}</h1>;
|
||||
}
|
||||
|
||||
const breadcrumbsResult = [];
|
||||
for (const bc of breadcrumbs) {
|
||||
if (bc.url) {
|
||||
breadcrumbsResult.push(
|
||||
<a className="text-link" key={breadcrumbsResult.length} href={bc.url}>
|
||||
{bc.title}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
breadcrumbsResult.push(<span key={breadcrumbsResult.length}> / {bc.title}</span>);
|
||||
}
|
||||
}
|
||||
breadcrumbsResult.push(<span key={breadcrumbs.length + 1}> / {title}</span>);
|
||||
|
||||
return <h1 className="page-header__title">{breadcrumbsResult}</h1>;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
headerCanvas: css`
|
||||
background: ${theme.colors.bg2};
|
||||
border-bottom: 1px solid ${theme.colors.border1};
|
||||
`,
|
||||
});
|
||||
|
||||
export default PageHeader;
|
||||
|
Reference in New Issue
Block a user