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.
|
||||
|
||||
/**
|
||||
* A hamburger menu element.
|
||||
* An enum of types of actions that might be requested
|
||||
* by the app.
|
||||
*/
|
||||
class HamburgerElement extends HTMLElement {
|
||||
attachedCallback() {
|
||||
this.innerHTML = '☰'; // 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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
})
|
||||
});
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"strictNullChecks": true
|
||||
"strictNullChecks": true,
|
||||
"target": "es2015"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue