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:
@@ -125,4 +125,5 @@ export interface GrafanaConfig {
|
|||||||
http2Enabled: boolean;
|
http2Enabled: boolean;
|
||||||
dateFormats?: SystemDateFormatSettings;
|
dateFormats?: SystemDateFormatSettings;
|
||||||
sentry: SentryConfig;
|
sentry: SentryConfig;
|
||||||
|
customTheme?: any;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
|||||||
};
|
};
|
||||||
marketplaceUrl?: string;
|
marketplaceUrl?: string;
|
||||||
expressionsEnabled = false;
|
expressionsEnabled = false;
|
||||||
|
customTheme?: any;
|
||||||
|
|
||||||
constructor(options: GrafanaBootConfig) {
|
constructor(options: GrafanaBootConfig) {
|
||||||
this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);
|
this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export type IconName =
|
|||||||
| 'cloud'
|
| 'cloud'
|
||||||
| 'draggabledots'
|
| 'draggabledots'
|
||||||
| 'folder-upload'
|
| 'folder-upload'
|
||||||
|
| 'palette'
|
||||||
| 'gf-interpolation-linear'
|
| 'gf-interpolation-linear'
|
||||||
| 'gf-interpolation-smooth'
|
| 'gf-interpolation-smooth'
|
||||||
| 'gf-interpolation-step-before'
|
| 'gf-interpolation-step-before'
|
||||||
@@ -246,6 +247,7 @@ export const getAvailableIcons = (): IconName[] => [
|
|||||||
'cloud',
|
'cloud',
|
||||||
'draggabledots',
|
'draggabledots',
|
||||||
'folder-upload',
|
'folder-upload',
|
||||||
|
'palette',
|
||||||
'gf-interpolation-linear',
|
'gf-interpolation-linear',
|
||||||
'gf-interpolation-smooth',
|
'gf-interpolation-smooth',
|
||||||
'gf-interpolation-step-before',
|
'gf-interpolation-step-before',
|
||||||
|
|||||||
@@ -19,50 +19,56 @@ import { createStyle } from '../../Theme';
|
|||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
|
|
||||||
export const getStyles = createStyle(() => {
|
export const getStyles = createStyle(() => {
|
||||||
const ScrubberHandleExpansion = css`
|
|
||||||
label: ScrubberHandleExpansion;
|
|
||||||
cursor: col-resize;
|
|
||||||
fill-opacity: 0;
|
|
||||||
fill: #44f;
|
|
||||||
`;
|
|
||||||
const ScrubberHandle = css`
|
|
||||||
label: ScrubberHandle;
|
|
||||||
cursor: col-resize;
|
|
||||||
fill: #555;
|
|
||||||
`;
|
|
||||||
const ScrubberLine = css`
|
|
||||||
label: ScrubberLine;
|
|
||||||
pointer-events: none;
|
|
||||||
stroke: #555;
|
|
||||||
`;
|
|
||||||
return {
|
return {
|
||||||
|
ScrubberHandleExpansion: cx(
|
||||||
|
css`
|
||||||
|
label: ScrubberHandleExpansion;
|
||||||
|
cursor: col-resize;
|
||||||
|
fill-opacity: 0;
|
||||||
|
fill: #44f;
|
||||||
|
`,
|
||||||
|
'scrubber-handle-expansion'
|
||||||
|
),
|
||||||
|
ScrubberHandle: cx(
|
||||||
|
css`
|
||||||
|
label: ScrubberHandle;
|
||||||
|
cursor: col-resize;
|
||||||
|
fill: #555;
|
||||||
|
`,
|
||||||
|
'scrubber-handle'
|
||||||
|
),
|
||||||
|
ScrubberLine: cx(
|
||||||
|
css`
|
||||||
|
label: ScrubberLine;
|
||||||
|
pointer-events: none;
|
||||||
|
stroke: #555;
|
||||||
|
`,
|
||||||
|
'scrubber-line'
|
||||||
|
),
|
||||||
ScrubberDragging: css`
|
ScrubberDragging: css`
|
||||||
label: ScrubberDragging;
|
label: ScrubberDragging;
|
||||||
& .${ScrubberHandleExpansion} {
|
& .scrubber-handle-expansion {
|
||||||
fill-opacity: 1;
|
fill-opacity: 1;
|
||||||
}
|
}
|
||||||
& .${ScrubberHandle} {
|
& .scrubber-handle {
|
||||||
fill: #44f;
|
fill: #44f;
|
||||||
}
|
}
|
||||||
& > .${ScrubberLine} {
|
& > .scrubber-line {
|
||||||
stroke: #44f;
|
stroke: #44f;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
ScrubberHandles: css`
|
ScrubberHandles: css`
|
||||||
label: ScrubberHandles;
|
label: ScrubberHandles;
|
||||||
&:hover > .${ScrubberHandleExpansion} {
|
&:hover > .scrubber-handle-expansion {
|
||||||
fill-opacity: 1;
|
fill-opacity: 1;
|
||||||
}
|
}
|
||||||
&:hover > .${ScrubberHandle} {
|
&:hover > .scrubber-handle {
|
||||||
fill: #44f;
|
fill: #44f;
|
||||||
}
|
}
|
||||||
&:hover + .${ScrubberLine} {
|
&:hover + .scrubber.line {
|
||||||
stroke: #44f;
|
stroke: #44f;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
ScrubberHandleExpansion,
|
|
||||||
ScrubberHandle,
|
|
||||||
ScrubberLine,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,10 +36,6 @@ import { createStyle } from '../Theme';
|
|||||||
import { uTxMuted } from '../uberUtilityStyles';
|
import { uTxMuted } from '../uberUtilityStyles';
|
||||||
|
|
||||||
const getStyles = createStyle((theme: Theme) => {
|
const getStyles = createStyle((theme: Theme) => {
|
||||||
const TracePageHeaderOverviewItemValueDetail = css`
|
|
||||||
label: TracePageHeaderOverviewItemValueDetail;
|
|
||||||
color: #aaa;
|
|
||||||
`;
|
|
||||||
return {
|
return {
|
||||||
TracePageHeader: css`
|
TracePageHeader: css`
|
||||||
label: TracePageHeader;
|
label: TracePageHeader;
|
||||||
@@ -117,10 +113,16 @@ const getStyles = createStyle((theme: Theme) => {
|
|||||||
border-bottom: 1px solid #e4e4e4;
|
border-bottom: 1px solid #e4e4e4;
|
||||||
padding: 0.25rem 0.5rem !important;
|
padding: 0.25rem 0.5rem !important;
|
||||||
`,
|
`,
|
||||||
TracePageHeaderOverviewItemValueDetail,
|
TracePageHeaderOverviewItemValueDetail: cx(
|
||||||
|
css`
|
||||||
|
label: TracePageHeaderOverviewItemValueDetail;
|
||||||
|
color: #aaa;
|
||||||
|
`,
|
||||||
|
'trace-item-value-detail'
|
||||||
|
),
|
||||||
TracePageHeaderOverviewItemValue: css`
|
TracePageHeaderOverviewItemValue: css`
|
||||||
label: TracePageHeaderOverviewItemValue;
|
label: TracePageHeaderOverviewItemValue;
|
||||||
&:hover > .${TracePageHeaderOverviewItemValueDetail} {
|
&:hover > .trace-item-value-detail {
|
||||||
color: unset;
|
color: unset;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -163,13 +165,13 @@ export const HEADER_ITEMS = [
|
|||||||
{
|
{
|
||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
label: 'Trace Start',
|
label: 'Trace Start',
|
||||||
renderer(trace: Trace, styles?: ReturnType<typeof getStyles>) {
|
renderer(trace: Trace, styles: ReturnType<typeof getStyles>) {
|
||||||
const dateStr = formatDatetime(trace.startTime);
|
const dateStr = formatDatetime(trace.startTime);
|
||||||
const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/);
|
const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/);
|
||||||
return match ? (
|
return match ? (
|
||||||
<span className={styles?.TracePageHeaderOverviewItemValue}>
|
<span className={styles.TracePageHeaderOverviewItemValue}>
|
||||||
{match[1]}
|
{match[1]}
|
||||||
<span className={styles?.TracePageHeaderOverviewItemValueDetail}>{match[2]}</span>
|
<span className={styles.TracePageHeaderOverviewItemValueDetail}>{match[2]}</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
dateStr
|
dateStr
|
||||||
|
|||||||
@@ -62,3 +62,6 @@ type NavLink struct {
|
|||||||
HideFromTabs bool `json:"hideFromTabs,omitempty"`
|
HideFromTabs bool `json:"hideFromTabs,omitempty"`
|
||||||
Children []*NavLink `json:"children,omitempty"`
|
Children []*NavLink `json:"children,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NavIDCfg is the id for org configuration navigation node
|
||||||
|
const NavIDCfg = "cfg"
|
||||||
|
|||||||
+1
-1
@@ -282,7 +282,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
|
|
||||||
if len(configNodes) > 0 {
|
if len(configNodes) > 0 {
|
||||||
navTree = append(navTree, &dtos.NavLink{
|
navTree = append(navTree, &dtos.NavLink{
|
||||||
Id: "cfg",
|
Id: dtos.NavIDCfg,
|
||||||
Text: "Configuration",
|
Text: "Configuration",
|
||||||
SubTitle: "Organization: " + c.OrgName,
|
SubTitle: "Organization: " + c.OrgName,
|
||||||
Icon: "cog",
|
Icon: "cog",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { LinkButton } from '@grafana/ui';
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
setSearchQuery: (value: string) => {};
|
setSearchQuery: (value: string) => void;
|
||||||
linkButton: { href: string; title: string };
|
linkButton: { href: string; title: string };
|
||||||
target?: string;
|
target?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,58 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { Component, HTMLAttributes } from 'react';
|
import React, { FC, HTMLAttributes, useEffect } from 'react';
|
||||||
import { getTitleFromNavModel } from 'app/core/selectors/navModel';
|
import { getTitleFromNavModel } from 'app/core/selectors/navModel';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import PageHeader from '../PageHeader/PageHeader';
|
import PageHeader from '../PageHeader/PageHeader';
|
||||||
import { Footer } from '../Footer/Footer';
|
import { Footer } from '../Footer/Footer';
|
||||||
import PageContents from './PageContents';
|
import { PageContents } from './PageContents';
|
||||||
import { CustomScrollbar } from '@grafana/ui';
|
import { CustomScrollbar, useStyles } from '@grafana/ui';
|
||||||
import { NavModel } from '@grafana/data';
|
import { GrafanaTheme, NavModel } from '@grafana/data';
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
import { Branding } from '../Branding/Branding';
|
import { Branding } from '../Branding/Branding';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Page extends Component<Props> {
|
export interface PageType extends FC<Props> {
|
||||||
static Header = PageHeader;
|
Header: typeof PageHeader;
|
||||||
static Contents = PageContents;
|
Contents: typeof 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 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;
|
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
|
// Libraries
|
||||||
import React, { Component } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import PageLoader from '../PageLoader/PageLoader';
|
import PageLoader from '../PageLoader/PageLoader';
|
||||||
@@ -9,12 +9,6 @@ interface Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PageContents extends Component<Props> {
|
export const PageContents: FC<Props> = ({ isLoading, children }) => {
|
||||||
render() {
|
return <div className="page-container page-body">{isLoading ? <PageLoader /> : children}</div>;
|
||||||
const { isLoading } = this.props;
|
};
|
||||||
|
|
||||||
return <div className="page-container page-body">{isLoading ? <PageLoader /> : this.props.children}</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PageContents;
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PageHeader from './PageHeader';
|
import PageHeader from './PageHeader';
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
describe('PageHeader', () => {
|
describe('PageHeader', () => {
|
||||||
let wrapper: ShallowWrapper<PageHeader>;
|
|
||||||
|
|
||||||
describe('when the nav tree has a node with a title', () => {
|
describe('when the nav tree has a node with a title', () => {
|
||||||
beforeAll(() => {
|
it('should render the title', async () => {
|
||||||
const nav = {
|
const nav = {
|
||||||
main: {
|
main: {
|
||||||
icon: 'folder-open',
|
icon: 'folder-open',
|
||||||
@@ -17,17 +15,15 @@ describe('PageHeader', () => {
|
|||||||
},
|
},
|
||||||
node: {},
|
node: {},
|
||||||
};
|
};
|
||||||
wrapper = shallow(<PageHeader model={nav as any} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the title', () => {
|
render(<PageHeader model={nav as any} />);
|
||||||
const title = wrapper.find('.page-header__title');
|
|
||||||
expect(title.text()).toBe('node');
|
expect(screen.getByRole('heading', { name: 'node' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the nav tree has a node with breadcrumbs and a title', () => {
|
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 = {
|
const nav = {
|
||||||
main: {
|
main: {
|
||||||
icon: 'folder-open',
|
icon: 'folder-open',
|
||||||
@@ -39,15 +35,11 @@ describe('PageHeader', () => {
|
|||||||
},
|
},
|
||||||
node: {},
|
node: {},
|
||||||
};
|
};
|
||||||
wrapper = shallow(<PageHeader model={nav as any} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the title with breadcrumbs first and then title last', () => {
|
render(<PageHeader model={nav as any} />);
|
||||||
const title = wrapper.find('.page-header__title');
|
|
||||||
expect(title.text()).toBe('Parent / child');
|
|
||||||
|
|
||||||
const parentLink = wrapper.find('.page-header__title > a.text-link');
|
expect(screen.getByRole('heading', { name: 'Parent / child' })).toBeInTheDocument();
|
||||||
expect(parentLink.prop('href')).toBe('parentUrl');
|
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 { css } from 'emotion';
|
||||||
import { Tab, TabsBar, Icon, IconName } from '@grafana/ui';
|
import { Tab, TabsBar, Icon, IconName, useStyles } from '@grafana/ui';
|
||||||
import { NavModel, NavModelItem, NavModelBreadcrumb } from '@grafana/data';
|
import { NavModel, NavModelItem, NavModelBreadcrumb, GrafanaTheme } from '@grafana/data';
|
||||||
import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem';
|
import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@@ -71,86 +71,77 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class PageHeader extends React.Component<Props, any> {
|
export const PageHeader: FC<Props> = ({ model }) => {
|
||||||
constructor(props: Props) {
|
const styles = useStyles(getStyles);
|
||||||
super(props);
|
|
||||||
|
if (!model) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
const main = model.main;
|
||||||
//Hack to re-render on changed props from angular with the @observer decorator
|
const children = main.children;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTitle(title: string, breadcrumbs: NavModelBreadcrumb[]) {
|
return (
|
||||||
if (!title && (!breadcrumbs || breadcrumbs.length === 0)) {
|
<div className={styles.headerCanvas}>
|
||||||
return null;
|
<div className="page-container">
|
||||||
}
|
<div className="page-header">
|
||||||
|
{renderHeaderTitle(main)}
|
||||||
if (!breadcrumbs || breadcrumbs.length === 0) {
|
{children && children.length && <Navigation>{children}</Navigation>}
|
||||||
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>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
function renderHeaderTitle(main: NavModelItem) {
|
||||||
const { model } = this.props;
|
const marginTop = main.icon === 'grafana' ? 12 : 14;
|
||||||
|
|
||||||
if (!model) {
|
return (
|
||||||
return null;
|
<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;
|
<div className="page-header__info-block">
|
||||||
const children = main.children;
|
{renderTitle(main.text, main.breadcrumbs ?? [])}
|
||||||
|
{main.subTitle && <div className="page-header__sub-title">{main.subTitle}</div>}
|
||||||
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>
|
</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;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { config, GrafanaBootConfig } from '@grafana/runtime';
|
import { config, GrafanaBootConfig } from '@grafana/runtime';
|
||||||
import { ThemeContext, getTheme } from '@grafana/ui';
|
import { ThemeContext } from '@grafana/ui';
|
||||||
import { GrafanaThemeType } from '@grafana/data';
|
import { appEvents } from '../core';
|
||||||
|
import { ThemeChangedEvent } from 'app/types/events';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
export const ConfigContext = React.createContext<GrafanaBootConfig>(config);
|
export const ConfigContext = React.createContext<GrafanaBootConfig>(config);
|
||||||
export const ConfigConsumer = ConfigContext.Consumer;
|
export const ConfigConsumer = ConfigContext.Consumer;
|
||||||
@@ -10,23 +12,22 @@ export const provideConfig = (component: React.ComponentType<any>) => {
|
|||||||
const ConfigProvider = (props: any) => (
|
const ConfigProvider = (props: any) => (
|
||||||
<ConfigContext.Provider value={config}>{React.createElement(component, { ...props })}</ConfigContext.Provider>
|
<ConfigContext.Provider value={config}>{React.createElement(component, { ...props })}</ConfigContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
return ConfigProvider;
|
return ConfigProvider;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCurrentThemeName = () =>
|
|
||||||
config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark;
|
|
||||||
|
|
||||||
export const getCurrentTheme = () => getTheme(getCurrentThemeName());
|
|
||||||
|
|
||||||
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
const [theme, setTheme] = useState<GrafanaTheme>(config.theme);
|
||||||
<ConfigConsumer>
|
|
||||||
{(config) => {
|
useEffect(() => {
|
||||||
return <ThemeContext.Provider value={getCurrentTheme()}>{children}</ThemeContext.Provider>;
|
const sub = appEvents.subscribe(ThemeChangedEvent, (event) => {
|
||||||
}}
|
config.theme = event.payload;
|
||||||
</ConfigConsumer>
|
setTheme(event.payload);
|
||||||
);
|
});
|
||||||
|
|
||||||
|
return () => sub.unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const provideTheme = (component: React.ComponentType<any>) => {
|
export const provideTheme = (component: React.ComponentType<any>) => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { GraphLegendProps, Legend } from './Legend/Legend';
|
|||||||
|
|
||||||
import { GraphCtrl } from './module';
|
import { GraphCtrl } from './module';
|
||||||
import { graphTickFormatter, graphTimeFormat, IconName, MenuItem, MenuItemsGroup } from '@grafana/ui';
|
import { graphTickFormatter, graphTimeFormat, IconName, MenuItem, MenuItemsGroup } from '@grafana/ui';
|
||||||
import { getCurrentTheme, provideTheme } from 'app/core/utils/ConfigProvider';
|
import { provideTheme } from 'app/core/utils/ConfigProvider';
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
DataFrameView,
|
DataFrameView,
|
||||||
@@ -284,7 +284,7 @@ class GraphElement {
|
|||||||
};
|
};
|
||||||
const fieldDisplay = getDisplayProcessor({
|
const fieldDisplay = getDisplayProcessor({
|
||||||
field: { config: fieldConfig, type: FieldType.number },
|
field: { config: fieldConfig, type: FieldType.number },
|
||||||
theme: getCurrentTheme(),
|
theme: config.theme,
|
||||||
timeZone: this.dashboard.getTimezone(),
|
timeZone: this.dashboard.getTimezone(),
|
||||||
})(field.values.get(dataIndex));
|
})(field.values.get(dataIndex));
|
||||||
linksSupplier = links.length
|
linksSupplier = links.length
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BusEventBase, eventFactory, TimeRange } from '@grafana/data';
|
import { BusEventBase, BusEventWithPayload, eventFactory, GrafanaTheme, TimeRange } from '@grafana/data';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,3 +156,7 @@ export class RefreshEvent extends BusEventBase {
|
|||||||
export class RenderEvent extends BusEventBase {
|
export class RenderEvent extends BusEventBase {
|
||||||
static type = 'render';
|
static type = 'render';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ThemeChangedEvent extends BusEventWithPayload<GrafanaTheme> {
|
||||||
|
static type = 'theme-changed';
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
.page-header-canvas {
|
|
||||||
background: $page-header-bg;
|
|
||||||
box-shadow: $page-header-shadow;
|
|
||||||
border-bottom: 1px solid $page-header-border-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
padding: $space-xl 0 0 0;
|
padding: $space-xl 0 0 0;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user