mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Footer: Single footer component for both react & angular pages (#21389)
* Footer: Single footer implementation for both react & angular pages * Export type * Updates * Use footer links in help menu * Updates & Fixes * Updated snapshot * updated snapshot
This commit is contained in:
18
public/app/core/components/Branding/Branding.tsx
Normal file
18
public/app/core/components/Branding/Branding.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
export interface BrandComponentProps {
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const LogoIcon: FC<BrandComponentProps> = ({ className }) => {
|
||||
return <img className={className} src="public/img/grafana_icon.svg" alt="Grafana" />;
|
||||
};
|
||||
|
||||
export const Wordmark: FC<BrandComponentProps> = ({ className }) => {
|
||||
return <div className={className} />;
|
||||
};
|
||||
|
||||
export class Branding {
|
||||
static LogoIcon = LogoIcon;
|
||||
static Wordmark = Wordmark;
|
||||
}
|
@@ -1,61 +1,77 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import config from 'app/core/config';
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
buildVersion: string;
|
||||
buildCommit: string;
|
||||
newGrafanaVersionExists: boolean;
|
||||
newGrafanaVersion: string;
|
||||
export interface FooterLink {
|
||||
text: string;
|
||||
icon?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export const Footer: FC<Props> = React.memo(
|
||||
({ appName, buildVersion, buildCommit, newGrafanaVersionExists, newGrafanaVersion }) => {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="text-center">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="http://docs.grafana.org" target="_blank" rel="noopener">
|
||||
<i className="fa fa-file-code-o" /> Docs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://grafana.com/products/enterprise/?utm_source=grafana_footer"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<i className="fa fa-support" /> Support & Enterprise
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://community.grafana.com/" target="_blank" rel="noopener">
|
||||
<i className="fa fa-comments-o" /> Community
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://grafana.com" target="_blank" rel="noopener">
|
||||
{appName}
|
||||
</a>{' '}
|
||||
<span>
|
||||
v{buildVersion} (commit: {buildCommit})
|
||||
</span>
|
||||
</li>
|
||||
{newGrafanaVersionExists && (
|
||||
<li>
|
||||
<Tooltip placement="auto" content={newGrafanaVersion}>
|
||||
<a href="https://grafana.com/get" target="_blank" rel="noopener">
|
||||
New version available!
|
||||
</a>
|
||||
</Tooltip>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
);
|
||||
export let getFooterLinks = (): FooterLink[] => {
|
||||
return [
|
||||
{
|
||||
text: 'Docs',
|
||||
icon: 'fa fa-file-code-o',
|
||||
url: 'https://grafana.com/docs/grafana/latest/?utm_source=grafana_footer',
|
||||
},
|
||||
{
|
||||
text: 'Support & Enterprise',
|
||||
icon: 'fa fa-support',
|
||||
url: 'https://grafana.com/products/enterprise/?utm_source=grafana_footer',
|
||||
},
|
||||
{
|
||||
text: 'Community',
|
||||
icon: 'fa fa-comments-o',
|
||||
url: 'https://community.grafana.com/?utm_source=grafana_footer',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
export let getVersionLinks = (): FooterLink[] => {
|
||||
const { buildInfo } = config;
|
||||
|
||||
const links: FooterLink[] = [
|
||||
{
|
||||
text: `Grafana v${buildInfo.version} (commit: ${buildInfo.commit})`,
|
||||
url: 'https://grafana.com',
|
||||
},
|
||||
];
|
||||
|
||||
if (buildInfo.hasUpdate) {
|
||||
links.push({
|
||||
text: `New version available!`,
|
||||
icon: 'fa fa-download',
|
||||
url: 'https://grafana.com/grafana/download?utm_source=grafana_footer',
|
||||
});
|
||||
}
|
||||
|
||||
return links;
|
||||
};
|
||||
|
||||
export function setFooterLinksFn(fn: typeof getFooterLinks) {
|
||||
getFooterLinks = fn;
|
||||
}
|
||||
|
||||
export function setVersionLinkFn(fn: typeof getFooterLinks) {
|
||||
getVersionLinks = fn;
|
||||
}
|
||||
|
||||
export const Footer: FC = React.memo(() => {
|
||||
const links = getFooterLinks().concat(getVersionLinks());
|
||||
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="text-center">
|
||||
<ul>
|
||||
{links.map(link => (
|
||||
<li key={link.text}>
|
||||
<a href={link.url} target="_blank" rel="noopener">
|
||||
<i className={link.icon} /> {link.text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
});
|
||||
|
@@ -5,14 +5,15 @@ import LoginCtrl from './LoginCtrl';
|
||||
import { LoginForm } from './LoginForm';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import { Branding } from 'app/core/components/Branding/Branding';
|
||||
|
||||
export const LoginPage: FC = () => {
|
||||
return (
|
||||
<div className="login container">
|
||||
<div className="login-content">
|
||||
<div className="login-branding">
|
||||
<img className="logo-icon" src="public/img/grafana_icon.svg" alt="Grafana" />
|
||||
<div className="logo-wordmark" />
|
||||
<Branding.LogoIcon className="logo-icon" />
|
||||
<Branding.Wordmark className="logo-wordmark" />
|
||||
</div>
|
||||
<LoginCtrl>
|
||||
{({
|
||||
|
@@ -1,11 +1,10 @@
|
||||
// Libraries
|
||||
import React, { Component } from 'react';
|
||||
import config from 'app/core/config';
|
||||
import { getTitleFromNavModel } from 'app/core/selectors/navModel';
|
||||
|
||||
// Components
|
||||
import PageHeader from '../PageHeader/PageHeader';
|
||||
import Footer from '../Footer/Footer';
|
||||
import { Footer } from '../Footer/Footer';
|
||||
import PageContents from './PageContents';
|
||||
import { CustomScrollbar } from '@grafana/ui';
|
||||
import { NavModel } from '@grafana/data';
|
||||
@@ -45,20 +44,13 @@ class Page extends Component<Props> {
|
||||
|
||||
render() {
|
||||
const { navModel } = this.props;
|
||||
const { buildInfo } = config;
|
||||
return (
|
||||
<div className="page-scrollbar-wrapper">
|
||||
<CustomScrollbar autoHeightMin={'100%'} className="custom-scrollbar--page">
|
||||
<div className="page-scrollbar-content">
|
||||
<PageHeader model={navModel} />
|
||||
{this.props.children}
|
||||
<Footer
|
||||
appName="Grafana"
|
||||
buildCommit={buildInfo.commit}
|
||||
buildVersion={buildInfo.version}
|
||||
newGrafanaVersion={buildInfo.latestVersion}
|
||||
newGrafanaVersionExists={buildInfo.hasUpdate}
|
||||
/>
|
||||
<Footer />
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
|
@@ -90,11 +90,9 @@ describe('Render', () => {
|
||||
describe('Functions', () => {
|
||||
describe('item clicked', () => {
|
||||
const wrapper = setup();
|
||||
const mockEvent = { preventDefault: jest.fn() };
|
||||
it('should emit show modal event if url matches shortcut', () => {
|
||||
const child = { url: '/shortcuts', text: 'hello' };
|
||||
const instance = wrapper.instance() as BottomNavLinks;
|
||||
instance.itemClicked(mockEvent as any, child);
|
||||
instance.onOpenShortcuts();
|
||||
|
||||
expect(appEvents.emit).toHaveBeenCalledWith(CoreEvents.showModal, { templateHtml: '<help-modal></help-modal>' });
|
||||
});
|
||||
|
@@ -4,6 +4,7 @@ import { User } from '../../services/context_srv';
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { CoreEvents } from 'app/types';
|
||||
import { OrgSwitcher } from '../OrgSwitcher';
|
||||
import { getFooterLinks } from '../Footer/Footer';
|
||||
|
||||
export interface Props {
|
||||
link: NavModelItem;
|
||||
@@ -19,13 +20,10 @@ class BottomNavLinks extends PureComponent<Props, State> {
|
||||
showSwitcherModal: false,
|
||||
};
|
||||
|
||||
itemClicked = (event: React.SyntheticEvent, child: NavModelItem) => {
|
||||
if (child.url === '/shortcuts') {
|
||||
event.preventDefault();
|
||||
appEvents.emit(CoreEvents.showModal, {
|
||||
templateHtml: '<help-modal></help-modal>',
|
||||
});
|
||||
}
|
||||
onOpenShortcuts = () => {
|
||||
appEvents.emit(CoreEvents.showModal, {
|
||||
templateHtml: '<help-modal></help-modal>',
|
||||
});
|
||||
};
|
||||
|
||||
toggleSwitcherModal = () => {
|
||||
@@ -38,6 +36,12 @@ class BottomNavLinks extends PureComponent<Props, State> {
|
||||
const { link, user } = this.props;
|
||||
const { showSwitcherModal } = this.state;
|
||||
|
||||
let children = link.children || [];
|
||||
|
||||
if (link.id === 'help') {
|
||||
children = getFooterLinks();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sidemenu-item dropdown dropup">
|
||||
<a href={link.url} className="sidemenu-link" target={link.target}>
|
||||
@@ -69,20 +73,25 @@ class BottomNavLinks extends PureComponent<Props, State> {
|
||||
|
||||
{showSwitcherModal && <OrgSwitcher onDismiss={this.toggleSwitcherModal} />}
|
||||
|
||||
{link.children &&
|
||||
link.children.map((child, index) => {
|
||||
if (!child.hideFromMenu) {
|
||||
return (
|
||||
<li key={`${child.text}-${index}`}>
|
||||
<a href={child.url} target={child.target} onClick={event => this.itemClicked(event, child)}>
|
||||
{child.icon && <i className={child.icon} />}
|
||||
{child.text}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
{children.map((child, index) => {
|
||||
return (
|
||||
<li key={`${child.text}-${index}`}>
|
||||
<a href={child.url} target="_blank" rel="noopener">
|
||||
{child.icon && <i className={child.icon} />}
|
||||
{child.text}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
{link.id === 'help' && (
|
||||
<li key="keyboard-shortcuts">
|
||||
<a onClick={() => this.onOpenShortcuts()}>
|
||||
<i className="fa fa-keyboard-o" /> Keyboard shortcuts
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li className="side-menu-header">
|
||||
<span className="sidemenu-item-text">{link.text}</span>
|
||||
</li>
|
||||
|
@@ -19,21 +19,32 @@ exports[`Render should render children 1`] = `
|
||||
key="undefined-0"
|
||||
>
|
||||
<a
|
||||
onClick={[Function]}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
key="undefined-1"
|
||||
>
|
||||
<a
|
||||
onClick={[Function]}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
key="undefined-2"
|
||||
>
|
||||
<a
|
||||
onClick={[Function]}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
key="undefined-3"
|
||||
>
|
||||
<a
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
|
Reference in New Issue
Block a user