x/tools/cmd/heapview: add a sidebar to hold navigation

This change also puts more structure into the viewer.
Adds an enum for events that we'll issue and a few more elements
to organize things.

Change-Id: I39c7c53422779348ca05f051c6b0b07d22ad6a00
Reviewed-on: https://go-review.googlesource.com/26656
Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
Michael Matloob 2016-08-10 11:43:06 -04:00
parent 3fe2afc9e6
commit 08b1e0510c
4 changed files with 187 additions and 27 deletions

View File

@ -3,47 +3,193 @@
// license that can be found in the LICENSE file.
/**
* A hamburger menu element.
* An enum of types of actions that might be requested
* by the app.
*/
class HamburgerElement extends HTMLElement {
attachedCallback() {
this.innerHTML = '&#9776'; // Unicode character for hamburger menu.
enum Action {
TOGGLE_SIDEBAR, // Toggle the sidebar.
NAVIGATE_ABOUT, // Go to the about page.
}
const TITLE = 'Go Heap Viewer';
/**
* A type of event that signals to the AppElement controller
* that something shoud be done. For the most part, the structure
* of the app will be that elements' state will mostly be controlled
* by parent elements. Elements will issue actions that the AppElement
* will handle, and the app will be re-rendered down the DOM
* hierarchy.
*/
class ActionEvent extends Event {
static readonly EVENT_TYPE = 'action-event'
constructor(public readonly action: Action) { super(ActionEvent.EVENT_TYPE); }
}
/**
* A hamburger menu element. Triggers a TOGGLE_SIDE action to toggle the
* sidebar.
*/
export class HamburgerElement extends HTMLElement {
static readonly NAME = 'heap-hamburger';
createdCallback() {
this.appendChild(document.createTextNode('☰'));
this.onclick =
() => { this.dispatchEvent(new ActionEvent(Action.TOGGLE_SIDEBAR)) };
}
}
document.registerElement('heap-hamburger', HamburgerElement);
document.registerElement(HamburgerElement.NAME, HamburgerElement);
/**
* A heading for the page with a hamburger menu and a title.
*/
export class HeadingElement extends HTMLElement {
attachedCallback() {
static readonly NAME = 'heap-heading';
createdCallback() {
this.style.display = 'block';
this.style.backgroundColor = '#2196F3';
this.style.webkitUserSelect = 'none';
this.style.cursor = 'default';
this.style.color = '#FFFFFF';
this.style.padding = '10px';
this.innerHTML = `
<div style="margin:0px; font-size:2em"><heap-hamburger></heap-hamburger> Go Heap Viewer</div>
`;
const div = document.createElement('div');
div.style.margin = '0px';
div.style.fontSize = '2em';
div.appendChild(document.createElement(HamburgerElement.NAME));
div.appendChild(document.createTextNode(' ' + TITLE));
this.appendChild(div);
}
}
document.registerElement('heap-heading', HeadingElement);
document.registerElement(HeadingElement.NAME, HeadingElement);
/**
* Reset body's margin and padding, and set font.
* A sidebar that has navigation for the app.
*/
function clearStyle() {
document.head.innerHTML += `
<style>
* {font-family: Roboto,Helvetica}
body {margin: 0px; padding:0px}
</style>
`;
export class SidebarElement extends HTMLElement {
static readonly NAME = 'heap-sidebar';
createdCallback() {
this.style.display = 'none';
this.style.backgroundColor = '#9E9E9E';
this.style.width = '15em';
const aboutButton = document.createElement('button');
aboutButton.innerText = 'about';
aboutButton.onclick =
() => { this.dispatchEvent(new ActionEvent(Action.NAVIGATE_ABOUT)) };
this.appendChild(aboutButton);
}
toggle() {
this.style.display = this.style.display === 'none' ? 'block' : 'none';
}
}
document.registerElement(SidebarElement.NAME, SidebarElement);
/**
* A Container for the main content in the app.
* TODO(matloob): Implement main content.
*/
export class MainContentElement extends HTMLElement {
static readonly NAME = 'heap-container';
attachedCallback() {
this.style.backgroundColor = '#E0E0E0';
this.style.height = '100%';
this.style.flex = '1';
}
}
document.registerElement(MainContentElement.NAME, MainContentElement);
/**
* A container and controller for the whole app.
* Contains the heading, side drawer and main panel.
*/
class AppElement extends HTMLElement {
static readonly NAME = 'heap-app';
private sidebar: SidebarElement;
private mainContent: MainContentElement;
attachedCallback() {
document.title = TITLE;
this.addEventListener(
ActionEvent.EVENT_TYPE, e => this.handleAction(e as ActionEvent),
/* capture */ true);
this.render();
}
render() {
this.style.display = 'block';
this.style.height = '100vh';
this.style.width = '100vw';
this.appendChild(document.createElement(HeadingElement.NAME));
const bodyDiv = document.createElement('div');
bodyDiv.style.height = '100%';
bodyDiv.style.display = 'flex';
this.sidebar =
document.createElement(SidebarElement.NAME) as SidebarElement;
bodyDiv.appendChild(this.sidebar);
this.mainContent =
document.createElement(MainContentElement.NAME) as MainContentElement;
bodyDiv.appendChild(this.mainContent);
this.appendChild(bodyDiv);
this.renderRoute();
}
renderRoute() {
this.mainContent.innerHTML = ''
switch (window.location.pathname) {
case '/about':
this.mainContent.appendChild(
document.createElement(AboutPageElement.NAME));
break;
}
}
handleAction(event: ActionEvent) {
switch (event.action) {
case Action.TOGGLE_SIDEBAR:
this.sidebar.toggle();
break;
case Action.NAVIGATE_ABOUT:
window.history.pushState({}, '', '/about');
this.renderRoute();
break;
}
}
}
document.registerElement(AppElement.NAME, AppElement);
/**
* An about page.
*/
class AboutPageElement extends HTMLElement {
static readonly NAME = 'heap-about';
createdCallback() { this.textContent = TITLE; }
}
document.registerElement(AboutPageElement.NAME, AboutPageElement);
/**
* Resets body's margin and padding, and sets font.
*/
function clearStyle(document: Document) {
const styleElement = document.createElement('style') as HTMLStyleElement;
document.head.appendChild(styleElement);
const styleSheet = styleElement.sheet as CSSStyleSheet;
styleSheet.insertRule(
'* {font-family: Roboto,Helvetica; box-sizing: border-box}', 0);
styleSheet.insertRule('body {margin: 0px; padding:0px}', 0);
}
export function main() {
document.title = 'Go Heap Viewer';
clearStyle();
document.body.appendChild(document.createElement("heap-heading"));
clearStyle(document);
document.body.appendChild(document.createElement(AppElement.NAME));
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import {main} from './main';
import {HamburgerElement, HeadingElement, SidebarElement, main} from './main';
describe('main', () => {
it('sets the document\'s title', () => {
@ -12,6 +12,18 @@ describe('main', () => {
it('has a heading', () => {
main();
expect(document.querySelector('heap-heading')).toBeDefined();
expect(document.querySelector(HeadingElement.NAME)).toBeDefined();
});
it('has a sidebar', () => {
main();
const hamburger = document.querySelector(HamburgerElement.NAME);
const sidebar =
document.querySelector(SidebarElement.NAME) as SidebarElement;
expect(sidebar.style.display).toBe('none');
// Click on the hamburger. Sidebar should then be visible.
hamburger.dispatchEvent(new Event('click'));
expect(sidebar.style.display).toBe('block');
})
});

View File

@ -10,6 +10,7 @@
{
"compilerOptions": {
"noEmit": true,
"strictNullChecks": true
"strictNullChecks": true,
"target": "es2015"
}
}

View File

@ -15,6 +15,7 @@ import (
"path/filepath"
)
var host = flag.String("host", "", "host addr to listen on")
var port = flag.Int("port", 8080, "service port")
var index = `<!DOCTYPE html>
@ -68,12 +69,12 @@ var addHandlers = func() {
})
}
var listenAndServe = func() {
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
var listenAndServe = func() error {
return http.ListenAndServe(fmt.Sprintf("%s:%d", *host, *port), nil)
}
func main() {
parseFlags()
addHandlers()
listenAndServe()
log.Fatal(listenAndServe())
}