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:
Torkel Ödegaard
2020-01-09 11:25:52 +01:00
committed by GitHub
parent 3866f609ce
commit 91ea3b15fa
40 changed files with 209 additions and 239 deletions

View File

@@ -352,11 +352,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
Icon: "gicon gicon-question", Icon: "gicon gicon-question",
HideFromMenu: true, HideFromMenu: true,
SortWeight: dtos.WeightHelp, SortWeight: dtos.WeightHelp,
Children: []*dtos.NavLink{ Children: []*dtos.NavLink{},
{Text: "Keyboard shortcuts", Url: "/shortcuts", Icon: "fa fa-fw fa-keyboard-o", Target: "_self"},
{Text: "Community site", Url: "http://community.grafana.com", Icon: "fa fa-fw fa-comment", Target: "_blank"},
{Text: "Documentation", Url: "http://docs.grafana.org", Icon: "fa fa-fw fa-file", Target: "_blank"},
},
}) })
hs.HooksService.RunIndexDataHooks(&data) hs.HooksService.RunIndexDataHooks(&data)

View File

@@ -21,8 +21,10 @@ import { GraphContextMenu } from 'app/plugins/panel/graph/GraphContextMenu';
import ReactProfileWrapper from 'app/features/profile/ReactProfileWrapper'; import ReactProfileWrapper from 'app/features/profile/ReactProfileWrapper';
import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor'; import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor';
import { HelpModal } from './components/help/HelpModal'; import { HelpModal } from './components/help/HelpModal';
import { Footer } from './components/Footer/Footer';
export function registerAngularDirectives() { export function registerAngularDirectives() {
react2AngularDirective('footer', Footer, []);
react2AngularDirective('helpModal', HelpModal, []); react2AngularDirective('helpModal', HelpModal, []);
react2AngularDirective('sidemenu', SideMenu, []); react2AngularDirective('sidemenu', SideMenu, []);
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']); react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);

View 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;
}

View File

@@ -1,61 +1,77 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { Tooltip } from '@grafana/ui'; import config from 'app/core/config';
interface Props { export interface FooterLink {
appName: string; text: string;
buildVersion: string; icon?: string;
buildCommit: string; url?: string;
newGrafanaVersionExists: boolean;
newGrafanaVersion: string;
} }
export const Footer: FC<Props> = React.memo( export let getFooterLinks = (): FooterLink[] => {
({ appName, buildVersion, buildCommit, newGrafanaVersionExists, newGrafanaVersion }) => { return [
return ( {
<footer className="footer"> text: 'Docs',
<div className="text-center"> icon: 'fa fa-file-code-o',
<ul> url: 'https://grafana.com/docs/grafana/latest/?utm_source=grafana_footer',
<li> },
<a href="http://docs.grafana.org" target="_blank" rel="noopener"> {
<i className="fa fa-file-code-o" /> Docs text: 'Support & Enterprise',
</a> icon: 'fa fa-support',
</li> url: 'https://grafana.com/products/enterprise/?utm_source=grafana_footer',
<li> },
<a {
href="https://grafana.com/products/enterprise/?utm_source=grafana_footer" text: 'Community',
target="_blank" icon: 'fa fa-comments-o',
rel="noopener" url: 'https://community.grafana.com/?utm_source=grafana_footer',
> },
<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 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>
);
});

View File

@@ -5,14 +5,15 @@ import LoginCtrl from './LoginCtrl';
import { LoginForm } from './LoginForm'; import { LoginForm } from './LoginForm';
import { ChangePassword } from './ChangePassword'; import { ChangePassword } from './ChangePassword';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import { Branding } from 'app/core/components/Branding/Branding';
export const LoginPage: FC = () => { export const LoginPage: FC = () => {
return ( return (
<div className="login container"> <div className="login container">
<div className="login-content"> <div className="login-content">
<div className="login-branding"> <div className="login-branding">
<img className="logo-icon" src="public/img/grafana_icon.svg" alt="Grafana" /> <Branding.LogoIcon className="logo-icon" />
<div className="logo-wordmark" /> <Branding.Wordmark className="logo-wordmark" />
</div> </div>
<LoginCtrl> <LoginCtrl>
{({ {({

View File

@@ -1,11 +1,10 @@
// Libraries // Libraries
import React, { Component } from 'react'; import React, { Component } from 'react';
import config from 'app/core/config';
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 } from '@grafana/ui';
import { NavModel } from '@grafana/data'; import { NavModel } from '@grafana/data';
@@ -45,20 +44,13 @@ class Page extends Component<Props> {
render() { render() {
const { navModel } = this.props; const { navModel } = this.props;
const { buildInfo } = config;
return ( return (
<div className="page-scrollbar-wrapper"> <div className="page-scrollbar-wrapper">
<CustomScrollbar autoHeightMin={'100%'} className="custom-scrollbar--page"> <CustomScrollbar autoHeightMin={'100%'} className="custom-scrollbar--page">
<div className="page-scrollbar-content"> <div className="page-scrollbar-content">
<PageHeader model={navModel} /> <PageHeader model={navModel} />
{this.props.children} {this.props.children}
<Footer <Footer />
appName="Grafana"
buildCommit={buildInfo.commit}
buildVersion={buildInfo.version}
newGrafanaVersion={buildInfo.latestVersion}
newGrafanaVersionExists={buildInfo.hasUpdate}
/>
</div> </div>
</CustomScrollbar> </CustomScrollbar>
</div> </div>

View File

@@ -90,11 +90,9 @@ describe('Render', () => {
describe('Functions', () => { describe('Functions', () => {
describe('item clicked', () => { describe('item clicked', () => {
const wrapper = setup(); const wrapper = setup();
const mockEvent = { preventDefault: jest.fn() };
it('should emit show modal event if url matches shortcut', () => { it('should emit show modal event if url matches shortcut', () => {
const child = { url: '/shortcuts', text: 'hello' };
const instance = wrapper.instance() as BottomNavLinks; 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>' }); expect(appEvents.emit).toHaveBeenCalledWith(CoreEvents.showModal, { templateHtml: '<help-modal></help-modal>' });
}); });

View File

@@ -4,6 +4,7 @@ import { User } from '../../services/context_srv';
import { NavModelItem } from '@grafana/data'; import { NavModelItem } from '@grafana/data';
import { CoreEvents } from 'app/types'; import { CoreEvents } from 'app/types';
import { OrgSwitcher } from '../OrgSwitcher'; import { OrgSwitcher } from '../OrgSwitcher';
import { getFooterLinks } from '../Footer/Footer';
export interface Props { export interface Props {
link: NavModelItem; link: NavModelItem;
@@ -19,13 +20,10 @@ class BottomNavLinks extends PureComponent<Props, State> {
showSwitcherModal: false, showSwitcherModal: false,
}; };
itemClicked = (event: React.SyntheticEvent, child: NavModelItem) => { onOpenShortcuts = () => {
if (child.url === '/shortcuts') { appEvents.emit(CoreEvents.showModal, {
event.preventDefault(); templateHtml: '<help-modal></help-modal>',
appEvents.emit(CoreEvents.showModal, { });
templateHtml: '<help-modal></help-modal>',
});
}
}; };
toggleSwitcherModal = () => { toggleSwitcherModal = () => {
@@ -38,6 +36,12 @@ class BottomNavLinks extends PureComponent<Props, State> {
const { link, user } = this.props; const { link, user } = this.props;
const { showSwitcherModal } = this.state; const { showSwitcherModal } = this.state;
let children = link.children || [];
if (link.id === 'help') {
children = getFooterLinks();
}
return ( return (
<div className="sidemenu-item dropdown dropup"> <div className="sidemenu-item dropdown dropup">
<a href={link.url} className="sidemenu-link" target={link.target}> <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} />} {showSwitcherModal && <OrgSwitcher onDismiss={this.toggleSwitcherModal} />}
{link.children && {children.map((child, index) => {
link.children.map((child, index) => { return (
if (!child.hideFromMenu) { <li key={`${child.text}-${index}`}>
return ( <a href={child.url} target="_blank" rel="noopener">
<li key={`${child.text}-${index}`}> {child.icon && <i className={child.icon} />}
<a href={child.url} target={child.target} onClick={event => this.itemClicked(event, child)}> {child.text}
{child.icon && <i className={child.icon} />} </a>
{child.text} </li>
</a> );
</li> })}
);
} {link.id === 'help' && (
return null; <li key="keyboard-shortcuts">
})} <a onClick={() => this.onOpenShortcuts()}>
<i className="fa fa-keyboard-o" /> Keyboard shortcuts
</a>
</li>
)}
<li className="side-menu-header"> <li className="side-menu-header">
<span className="sidemenu-item-text">{link.text}</span> <span className="sidemenu-item-text">{link.text}</span>
</li> </li>

View File

@@ -19,21 +19,32 @@ exports[`Render should render children 1`] = `
key="undefined-0" key="undefined-0"
> >
<a <a
onClick={[Function]} rel="noopener"
target="_blank"
/> />
</li> </li>
<li <li
key="undefined-1" key="undefined-1"
> >
<a <a
onClick={[Function]} rel="noopener"
target="_blank"
/> />
</li> </li>
<li <li
key="undefined-2" key="undefined-2"
> >
<a <a
onClick={[Function]} rel="noopener"
target="_blank"
/>
</li>
<li
key="undefined-3"
>
<a
rel="noopener"
target="_blank"
/> />
</li> </li>
<li <li

View File

@@ -1,29 +0,0 @@
import config from 'app/core/config';
import { BackendSrv } from 'app/core/services/backend_srv';
import { NavModelSrv } from 'app/core/core';
export default class StyleGuideCtrl {
theme: string;
buttonNames = ['primary', 'secondary', 'inverse', 'success', 'warning', 'danger'];
buttonSizes = ['btn-small', '', 'btn-large'];
buttonVariants = ['-'];
navModel: any;
/** @ngInject */
constructor(private $routeParams: any, private backendSrv: BackendSrv, navModelSrv: NavModelSrv) {
this.navModel = navModelSrv.getNav('admin', 'styleguide', 0);
this.theme = config.bootData.user.lightTheme ? 'light' : 'dark';
}
switchTheme() {
this.$routeParams.theme = this.theme === 'dark' ? 'light' : 'dark';
const cmd = {
theme: this.$routeParams.theme,
};
this.backendSrv.put('/api/user/preferences', cmd).then(() => {
window.location.href = window.location.href;
});
}
}

View File

@@ -159,14 +159,15 @@ exports[`ServerStats Should render table with stats 1`] = `
<ul> <ul>
<li> <li>
<a <a
href="http://docs.grafana.org" href="https://grafana.com/docs/grafana/latest/?utm_source=grafana_footer"
rel="noopener" rel="noopener"
target="_blank" target="_blank"
> >
<i <i
className="fa fa-file-code-o" className="fa fa-file-code-o"
/> />
Docs
Docs
</a> </a>
</li> </li>
<li> <li>
@@ -178,19 +179,21 @@ exports[`ServerStats Should render table with stats 1`] = `
<i <i
className="fa fa-support" className="fa fa-support"
/> />
Support & Enterprise
Support & Enterprise
</a> </a>
</li> </li>
<li> <li>
<a <a
href="https://community.grafana.com/" href="https://community.grafana.com/?utm_source=grafana_footer"
rel="noopener" rel="noopener"
target="_blank" target="_blank"
> >
<i <i
className="fa fa-comments-o" className="fa fa-comments-o"
/> />
Community
Community
</a> </a>
</li> </li>
<li> <li>
@@ -199,16 +202,10 @@ exports[`ServerStats Should render table with stats 1`] = `
rel="noopener" rel="noopener"
target="_blank" target="_blank"
> >
Grafana <i />
Grafana vv1.0 (commit: 1)
</a> </a>
<span>
v
v1.0
(commit:
1
)
</span>
</li> </li>
</ul> </ul>
</div> </div>

View File

@@ -2,7 +2,6 @@ import AdminListUsersCtrl from './AdminListUsersCtrl';
import AdminEditUserCtrl from './AdminEditUserCtrl'; import AdminEditUserCtrl from './AdminEditUserCtrl';
import AdminListOrgsCtrl from './AdminListOrgsCtrl'; import AdminListOrgsCtrl from './AdminListOrgsCtrl';
import AdminEditOrgCtrl from './AdminEditOrgCtrl'; import AdminEditOrgCtrl from './AdminEditOrgCtrl';
import StyleGuideCtrl from './StyleGuideCtrl';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import { NavModelSrv } from 'app/core/core'; import { NavModelSrv } from 'app/core/core';
@@ -21,4 +20,3 @@ coreModule.controller('AdminEditUserCtrl', AdminEditUserCtrl);
coreModule.controller('AdminListOrgsCtrl', AdminListOrgsCtrl); coreModule.controller('AdminListOrgsCtrl', AdminListOrgsCtrl);
coreModule.controller('AdminEditOrgCtrl', AdminEditOrgCtrl); coreModule.controller('AdminEditOrgCtrl', AdminEditOrgCtrl);
coreModule.controller('AdminHomeCtrl', AdminHomeCtrl); coreModule.controller('AdminHomeCtrl', AdminHomeCtrl);
coreModule.controller('StyleGuideCtrl', StyleGuideCtrl);

View File

@@ -9,3 +9,4 @@
</div> </div>
<footer />

View File

@@ -42,3 +42,5 @@
</tr> </tr>
</table> </table>
</div> </div>
<footer />

View File

@@ -181,3 +181,5 @@
</div> </div>
</div> </div>
</div> </div>
<footer />

View File

@@ -28,3 +28,5 @@
</div> </div>
</form> </form>
</div> </div>
<footer />

View File

@@ -37,3 +37,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<footer />

View File

@@ -76,3 +76,5 @@
</ol> </ol>
</div> </div>
</div> </div>
<footer />

View File

@@ -75,3 +75,5 @@
</div> </div>
</form> </form>
</div> </div>
<footer />

View File

@@ -57,3 +57,5 @@
/> />
</div> </div>
</div> </div>
<footer />

View File

@@ -32,3 +32,5 @@
</form> </form>
</div> </div>
<footer />

View File

@@ -1,5 +1,7 @@
<page-header ng-if="ctrl.navModel" model="ctrl.navModel"></page-header> <page-header ng-if="ctrl.navModel" model="ctrl.navModel"></page-header>
<div class="page-container page-body"> <div class="page-container page-body">
<manage-dashboards ng-if="ctrl.folderId && ctrl.uid" folder-id="ctrl.folderId" folder-uid="ctrl.uid" /> <manage-dashboards ng-if="ctrl.folderId && ctrl.uid" folder-id="ctrl.folderId" folder-uid="ctrl.uid" />
</div> </div>
<footer />

View File

@@ -157,3 +157,5 @@
</div> </div>
</div> </div>
<footer />

View File

@@ -3,3 +3,5 @@
<div class="page-container page-body"> <div class="page-container page-body">
<manage-dashboards /> <manage-dashboards />
</div> </div>
<footer />

View File

@@ -34,3 +34,5 @@
</table> </table>
</div> </div>
<footer />

View File

@@ -20,3 +20,5 @@
</div> </div>
</form> </form>
</div> </div>
<footer />

View File

@@ -24,3 +24,4 @@
</div> </div>
<footer />

View File

@@ -100,3 +100,5 @@
<a class="btn-text" ng-click="ctrl.backToList()">Cancel</a> <a class="btn-text" ng-click="ctrl.backToList()">Cancel</a>
</div> </div>
</div> </div>
<footer />

View File

@@ -57,10 +57,12 @@
title="'There are no playlists created yet'" title="'There are no playlists created yet'"
buttonIcon="'fa fa-plus'" buttonIcon="'fa fa-plus'"
buttonLink="'playlists/create'" buttonLink="'playlists/create'"
buttonTitle="'Create Playlist'" buttonTitle="'Create Playlist'"
proTip="'You can use playlists to cycle dashboards on TVs without user control'" proTip="'You can use playlists to cycle dashboards on TVs without user control'"
proTipLink="'http://docs.grafana.org/reference/playlist/'" proTipLink="'http://docs.grafana.org/reference/playlist/'"
proTipLinkTitle="'Learn more'" proTipLinkTitle="'Learn more'"
proTipTarget="'_blank'" /> proTipTarget="'_blank'" />
</div> </div>
</div> </div>
<footer />

View File

@@ -8,3 +8,5 @@
</div> </div>
</div> </div>
</div> </div>
<footer />

View File

@@ -32,3 +32,5 @@
</table> </table>
</div> </div>
</div> </div>
<footer />

View File

@@ -45,3 +45,5 @@
<span class="react-resizable-handle" style="cursor: default"></span> <span class="react-resizable-handle" style="cursor: default"></span>
</div> </div>
</div> </div>
<footer />

View File

@@ -54,3 +54,5 @@
</form> </form>
</div> </div>
</div> </div>
<footer />

View File

@@ -33,4 +33,4 @@
</form> </form>
</div> </div>
<footer />

View File

@@ -42,4 +42,6 @@
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<footer />

View File

@@ -378,11 +378,6 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
controller: 'AppPageCtrl', controller: 'AppPageCtrl',
controllerAs: 'ctrl', controllerAs: 'ctrl',
}) })
.when('/styleguide/:page?', {
controller: 'StyleGuideCtrl',
controllerAs: 'ctrl',
templateUrl: 'public/app/features/admin/partials/styleguide.html',
})
.when('/alerting', { .when('/alerting', {
redirectTo: '/alerting/list', redirectTo: '/alerting/list',
}) })

View File

@@ -103,7 +103,6 @@
@import 'pages/alerting'; @import 'pages/alerting';
@import 'pages/history'; @import 'pages/history';
@import 'pages/signup'; @import 'pages/signup';
@import 'pages/styleguide';
@import 'pages/errorpage'; @import 'pages/errorpage';
@import 'pages/explore'; @import 'pages/explore';
@import 'pages/plugins'; @import 'pages/plugins';

View File

@@ -15,6 +15,11 @@
&:hover { &:hover {
color: $footer-link-hover; color: $footer-link-hover;
} }
i {
display: inline-block;
padding-right: $space-xxs;
}
} }
ul { ul {
@@ -23,10 +28,10 @@
li { li {
display: inline-block; display: inline-block;
padding-right: 2px;
&::after { &::after {
content: ' | '; content: ' | ';
padding-left: 2px; padding: 0 $space-sm;
} }
} }
@@ -38,15 +43,6 @@
} }
} }
.is-react .footer {
display: none;
}
.is-react .custom-scrollbar .footer {
display: block;
}
// Keeping footer inside the graphic on Login screen
.login-page { .login-page {
.footer { .footer {
display: block; display: block;

View File

@@ -1,33 +0,0 @@
.style-guide-color-card {
list-style: none;
margin: 0;
padding: $spacer;
width: 100%;
text-align: left;
text-shadow: 0 0 8px #fff;
color: $black;
font-size: $font-size-sm;
}
.color-card-body-bg {
background-color: $body-bg;
}
.color-card-page-bg {
background-color: $page-bg;
}
.color-card-gray {
background-color: $gray-1;
}
.style-guide-button-list {
padding: $spacer;
button {
display: block;
margin: 0 $spacer $spacer 0;
}
}
.style-guide-icon-list {
font-size: 1.8em;
text-align: center;
}

View File

@@ -214,45 +214,7 @@
<dashboard-search></dashboard-search> <dashboard-search></dashboard-search>
<div class="main-view"> <div class="main-view">
<div class="scroll-canvas"> <div ng-view class="scroll-canvas"></div>
<div ng-view></div>
<footer class="footer">
<div class="text-center">
<ul>
<li>
<a href="http://docs.grafana.org" target="_blank">
<i class="fa fa-file-code-o"></i>
Docs
</a>
</li>
<li>
<a href="https://grafana.com/services/support" target="_blank">
<i class="fa fa-support"></i>
Support Plans
</a>
</li>
<li>
<a href="https://community.grafana.com/" target="_blank">
<i class="fa fa-comments-o"></i>
Community
</a>
</li>
<li>
<a href="https://grafana.com" target="_blank">[[.AppName]]</a>
<span>v[[.BuildVersion]] (commit: [[.BuildCommit]])</span>
</li>
[[if .NewGrafanaVersionExists]]
<li>
<a href="https://grafana.com/get" target="_blank" bs-tooltip="'[[.NewGrafanaVersion]]'">
New version available!
</a>
</li>
[[end]]
</ul>
</div>
</footer>
</div>
</div> </div>
</grafana-app> </grafana-app>