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:
parent
3fe2afc9e6
commit
08b1e0510c
|
|
@ -3,47 +3,193 @@
|
||||||
// license that can be found in the LICENSE file.
|
// 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 {
|
enum Action {
|
||||||
attachedCallback() {
|
TOGGLE_SIDEBAR, // Toggle the sidebar.
|
||||||
this.innerHTML = '☰'; // Unicode character for hamburger menu.
|
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.
|
* A heading for the page with a hamburger menu and a title.
|
||||||
*/
|
*/
|
||||||
export class HeadingElement extends HTMLElement {
|
export class HeadingElement extends HTMLElement {
|
||||||
attachedCallback() {
|
static readonly NAME = 'heap-heading';
|
||||||
|
|
||||||
|
createdCallback() {
|
||||||
this.style.display = 'block';
|
this.style.display = 'block';
|
||||||
this.style.backgroundColor = '#2196F3';
|
this.style.backgroundColor = '#2196F3';
|
||||||
this.style.webkitUserSelect = 'none';
|
this.style.webkitUserSelect = 'none';
|
||||||
this.style.cursor = 'default';
|
this.style.cursor = 'default';
|
||||||
this.style.color = '#FFFFFF';
|
this.style.color = '#FFFFFF';
|
||||||
this.style.padding = '10px';
|
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() {
|
export class SidebarElement extends HTMLElement {
|
||||||
document.head.innerHTML += `
|
static readonly NAME = 'heap-sidebar';
|
||||||
<style>
|
|
||||||
* {font-family: Roboto,Helvetica}
|
createdCallback() {
|
||||||
body {margin: 0px; padding:0px}
|
this.style.display = 'none';
|
||||||
</style>
|
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() {
|
export function main() {
|
||||||
document.title = 'Go Heap Viewer';
|
clearStyle(document);
|
||||||
clearStyle();
|
document.body.appendChild(document.createElement(AppElement.NAME));
|
||||||
document.body.appendChild(document.createElement("heap-heading"));
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
import {main} from './main';
|
import {HamburgerElement, HeadingElement, SidebarElement, main} from './main';
|
||||||
|
|
||||||
describe('main', () => {
|
describe('main', () => {
|
||||||
it('sets the document\'s title', () => {
|
it('sets the document\'s title', () => {
|
||||||
|
|
@ -12,6 +12,18 @@ describe('main', () => {
|
||||||
|
|
||||||
it('has a heading', () => {
|
it('has a heading', () => {
|
||||||
main();
|
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');
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"strictNullChecks": true
|
"strictNullChecks": true,
|
||||||
|
"target": "es2015"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var host = flag.String("host", "", "host addr to listen on")
|
||||||
var port = flag.Int("port", 8080, "service port")
|
var port = flag.Int("port", 8080, "service port")
|
||||||
|
|
||||||
var index = `<!DOCTYPE html>
|
var index = `<!DOCTYPE html>
|
||||||
|
|
@ -68,12 +69,12 @@ var addHandlers = func() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var listenAndServe = func() {
|
var listenAndServe = func() error {
|
||||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
|
return http.ListenAndServe(fmt.Sprintf("%s:%d", *host, *port), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
parseFlags()
|
parseFlags()
|
||||||
addHandlers()
|
addHandlers()
|
||||||
listenAndServe()
|
log.Fatal(listenAndServe())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue