playground/client: make results log toggle-able
* Results panel is toggle-able and hidden by default.
* Fix broken rendering in Safari.
Close https://github.com/vanadium/issues/issues/425
Close https://github.com/vanadium/issues/issues/426
Change-Id: Ia0556e73306410e0a5b0c3a2cfb501d6d10a020b
diff --git a/client/browser/api/index.js b/client/browser/api/index.js
index c8f74c6..09daa8a 100644
--- a/client/browser/api/index.js
+++ b/client/browser/api/index.js
@@ -88,7 +88,7 @@
clone.query = { id: encodeURIComponent(options.uuid) };
}
- if (api.options.debug) {
+ if (api.options.debug || options.debug) {
clone.query = clone.query || {};
clone.query.debug = 1;
}
@@ -207,7 +207,10 @@
API.prototype.run = function(data, callback) {
var api = this;
var uuid = data.uuid;
- var uri = api.url({ action: 'run' });
+ var uri = api.url({
+ action: 'run',
+ debug: true
+ });
if (api.isPending(uuid)) {
var message = format('%s is already running');
diff --git a/client/browser/components/bundle/index.js b/client/browser/components/bundle/index.js
index ac85a98..d31995f 100644
--- a/client/browser/components/bundle/index.js
+++ b/client/browser/components/bundle/index.js
@@ -27,6 +27,7 @@
stop: stop,
save: save,
fileChange: fileChange,
+ showResults: showResults
}
});
@@ -51,6 +52,7 @@
// If running clear previous logs and open the console.
if (running) {
+ state.results.open.set(true);
state.results.logs.set(hg.array([]));
state.results.follow.set(true);
}
@@ -59,6 +61,10 @@
return state;
}
+function showResults(state, data) {
+ state.results.open.set(true);
+}
+
// When a file's contents change via the editor update the state.
function fileChange(state, data) {
var current = state.files.get(data.name);
diff --git a/client/browser/components/bundle/render.js b/client/browser/components/bundle/render.js
index 746d1f8..4e547db 100644
--- a/client/browser/components/bundle/render.js
+++ b/client/browser/components/bundle/render.js
@@ -40,8 +40,18 @@
function tabs(state, channels) {
var files = toArray(state.files);
+ var children = files.map(tab, state);
- return h('.tabs', files.map(tab, state));
+ // .spacer for flex box pushing a.show-results to the far right
+ children.push(h('.spacer'));
+
+ children.push(h('a.show-results', {
+ title: 'Open the results console.',
+ href: '#',
+ 'ev-click': click(channels.showResults)
+ }));
+
+ return h('.tabs', children);
}
function tab(file, index, array) {
diff --git a/client/browser/components/log/render.js b/client/browser/components/log/render.js
index a064fbf..4dd85d6 100644
--- a/client/browser/components/log/render.js
+++ b/client/browser/components/log/render.js
@@ -8,7 +8,7 @@
// This is expected to be called an iterator fn passed to logs.map(render)
function render(state, index, logs) {
- var time = moment(state.timestamp).format('MMM D HH:mm:ss SSS');
+ var time = moment(state.timestamp).format('MMM D HH:mm:ss.SSS');
var stream = state.stream || 'unknown';
return h('.log', {
diff --git a/client/browser/components/results/index.js b/client/browser/components/results/index.js
index 95c287c..44c29fb 100644
--- a/client/browser/components/results/index.js
+++ b/client/browser/components/results/index.js
@@ -14,7 +14,8 @@
debug: hg.value(false),
channels: {
follow: follow,
- debug: debug
+ debug: debug,
+ toggle: toggle
}
});
@@ -40,3 +41,9 @@
state.debug.set(data.debug);
}
}
+
+function toggle(state, data) {
+ var current = state.open();
+
+ state.open.set(!current);
+}
diff --git a/client/browser/components/results/render.js b/client/browser/components/results/render.js
index 330917a..731d93e 100644
--- a/client/browser/components/results/render.js
+++ b/client/browser/components/results/render.js
@@ -8,20 +8,21 @@
var followHook = require('./follow-hook');
var log = require('../log');
var format = require('format');
+var debug = require('debug')('components:results:render');
module.exports = render;
function render(state, channels) {
- return h('.results', [
+ debug('update %o', state);
+
+ channels = channels || state.channels;
+
+ return h('.results', {
+ className: state.open ? 'opened' : 'closed'
+ },
+ [
hg.partial(controls, state, channels),
- h('.console', {
- className: state.debug ? 'debug' : ''
- }, [
- h('.scroller', {
- 'ev-scroll': scroll(channels.follow, { scrolling: true }),
- 'follow-console': followHook(state.follow)
- }, state.logs.map(log.render))
- ])
+ hg.partial(terminal, state, channels)
]);
}
@@ -30,12 +31,28 @@
var title = format('Toggle debug console output %s.', onOrOff);
var text = format(' Debug: %s', onOrOff);
- return h('.controls', [
- 'Results',
- h('a.debug', {
+ return h('.results-controls', [
+ h('a.toggle-display', {
+ href: '#',
+ title: (state.open ? 'Close' : 'Open') + ' the results console.',
+ 'ev-click': click(channels.toggle),
+ }),
+ h('.title', 'Results'),
+ h('a.debug-button', {
href: '#',
'ev-click': click(channels.debug, { debug: ! state.debug }),
title: title
- }, text)
+ }, text),
+ ]);
+}
+
+function terminal(state, channels) {
+ return h('.results-console', {
+ className: state.debug ? 'debug' : ''
+ }, [
+ h('.scroller', {
+ 'ev-scroll': scroll(channels.follow, { scrolling: true }),
+ 'follow-console': followHook(state.follow)
+ }, state.logs.map(log.render))
]);
}
diff --git a/client/package.json b/client/package.json
index 9185b9b..fdea47f 100644
--- a/client/package.json
+++ b/client/package.json
@@ -38,9 +38,11 @@
"minimist": "^1.1.1",
"myth": "^1.4.0",
"pgbundle": "0.0.1",
- "rework-inherit": "^0.2.3",
+ "raf": "^2.0.4",
"rework": "^1.0.1",
+ "rework-inherit": "^0.2.3",
"run-browser": "^2.0.2",
+ "synthetic-dom-events": "git://github.com/Raynos/synthetic-dom-events",
"tape": "^3.5.0"
},
"repository": {
diff --git a/client/public/icons/chevron-left.svg b/client/public/icons/chevron-left.svg
new file mode 100644
index 0000000..4048deb
--- /dev/null
+++ b/client/public/icons/chevron-left.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
+ <path d="M30.83 14.83L28 12 16 24l12 12 2.83-2.83L21.66 24z" fill="#ffffff" />
+ <path d="M0 0h48v48H0z" fill="none"/>
+</svg>
diff --git a/client/public/icons/chevron-right.svg b/client/public/icons/chevron-right.svg
new file mode 100644
index 0000000..764bff3
--- /dev/null
+++ b/client/public/icons/chevron-right.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
+ <path d="M20 12l-2.83 2.83L26.34 24l-9.17 9.17L20 36l12-12z" fill="#ffffff"/>
+ <path d="M0 0h48v48H0z" fill="none"/>
+</svg>
diff --git a/client/stylesheets/components/results.css b/client/stylesheets/components/results.css
new file mode 100644
index 0000000..c8312d0
--- /dev/null
+++ b/client/stylesheets/components/results.css
@@ -0,0 +1,127 @@
+.results {
+ display: flex;
+ flex-direction: column;
+ background-color: var(--grey-50);
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 35%;
+ right: 0;
+ z-index: 2000;
+ transition: transform ease-in-out 0.3s;
+}
+
+.results.closed {
+ transform: translateX(100%);
+}
+
+.results.opened {
+ transform: translateX(0);
+}
+
+.results-controls {
+ display: flex;
+ background-color: var(--cyan-600);
+ padding: var(--gutter-half) var(--gutter);
+ padding-left: var(--gutter-half);
+ color: var(--white);
+}
+
+.results-controls a.toggle-display {
+ inherit: .icon-base;
+ background-image: url(/icons/chevron-right.svg);
+}
+
+.results-controls a.toggle-display {
+ inherit: .icon-base;
+ margin-right: var(--gutter-half);
+}
+
+.opened .results-controls a.toggle-display {
+ background-image: url(/icons/chevron-right.svg);
+}
+
+.closed .results-controls a.toggle-display {
+ background-image: url(/icons/chevron-left.svg);
+}
+
+.results-controls .title {
+ inherit: .type-body;
+ margin: 0;
+ color: var(--white);
+ /* Push .debug-button to the end. */
+ flex: 1;
+}
+
+.results-controls .debug-button {
+ color: var(--blue-grey-700);
+}
+
+.results-console {
+ flex: 1;
+ position: relative;
+}
+
+.scroller {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: auto;
+ padding: var(--gutter);
+}
+
+.log {
+ margin-bottom: var(--gutter);
+}
+
+.log .meta {
+ display: flex;
+}
+
+.log .meta .source {
+ flex: 1;
+ font-weight: bold;
+}
+
+.log .meta .source .stream {
+ font-weight: normal;
+}
+
+.log .message {
+ font-family: "Source Code Pro", monospace;
+ padding-left: var(--gutter-half);
+ overflow: hidden;
+ overflow-y: scroll;
+}
+
+.log .message pre {
+ padding: 0;
+ margin: 0;
+ white-space: pre-wrap; /* CSS 3 */
+ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ word-wrap: break-word;
+}
+
+.log.debug pre {
+ color: #00B9F7;
+}
+
+.log.stdout {
+
+}
+
+.log.stderr pre {
+ color: #F03A76;
+}
+
+.log.debug {
+ display: none;
+}
+
+.results-console.debug .log.debug {
+ display: block;
+}
diff --git a/client/stylesheets/icons.css b/client/stylesheets/icons.css
index 5cc16cd..15ce74f 100644
--- a/client/stylesheets/icons.css
+++ b/client/stylesheets/icons.css
@@ -80,8 +80,8 @@
content: ' ';
display: inline-block;
/* matches typography line-height */
- width: 28px;
- height: 28px;
+ width: 24px;
+ height: 24px;
vertical-align: middle;
background-repeat: no-repeat;
background-position: center center;
diff --git a/client/stylesheets/index.css b/client/stylesheets/index.css
index 80bea7d..449c579 100644
--- a/client/stylesheets/index.css
+++ b/client/stylesheets/index.css
@@ -9,11 +9,13 @@
@import "./components/buttons.css";
@import "./components/header.css";
@import "./components/footer.css";
+@import "./components/results.css";
body, .playground {
display: flex;
min-height: 100vh;
flex-direction: column;
+ overflow: hidden;
}
main {
@@ -25,8 +27,8 @@
flex: 1;
display: flex;
flex-wrap: wrap;
- align-content: center;
justify-content: center;
+ position: relative;
}
.bundle .code,
@@ -45,6 +47,10 @@
background-color: var(--cyan-700);
}
+.bundle .tabs .spacer {
+ flex: 1;
+}
+
.bundle .tabs .tab {
padding: calc(var(--gutter-half) - 2px) var(--gutter);
border-bottom: 4px solid transparent;
@@ -56,6 +62,14 @@
color: var(--white);
}
+.bundle .tabs a.show-results {
+ inherit: .icon-base;
+ background-image: url(/icons/chevron-left.svg);
+ display: block;
+ margin: var(--gutter-half);
+ margin-right: var(--gutter);
+}
+
.editors {
flex: 1;
display: flex;
@@ -65,13 +79,17 @@
.editor {
flex: 1;
position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
width: 100%;
height: 100%;
- visibility: hidden;
+ display: none;
}
.editor.active {
- visibility: visible;
+ display: block;
}
.ace_editor {
@@ -79,91 +97,6 @@
height: 100%;
}
-.results {
- display: flex;
- flex-direction: column;
- background-color: var(--grey-50);
-}
-
-.results .controls {
- background-color: var(--cyan-600);
- padding: var(--gutter-half) var(--gutter);
- color: var(--white);
-}
-
-.results .controls a {
- color: var(--blue-grey-700);
-}
-
-.results .console {
- flex: 1;
- position: relative;
-}
-
-.scroller {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- overflow: auto;
- padding: var(--gutter);
-}
-
-.log {
- margin-bottom: var(--gutter);
-}
-
-.log .meta {
- display: flex;
-}
-
-.log .meta .source {
- flex: 1;
- font-weight: bold;
-}
-
-.log .meta .source .stream {
- font-weight: normal;
-}
-
-.log .message {
- font-family: "Source Code Pro", monospace;
- padding-left: var(--gutter-half);
- overflow: hidden;
- overflow-y: scroll;
-}
-
-.log .message pre {
- padding: 0;
- margin: 0;
- white-space: pre-wrap; /* CSS 3 */
- white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
- white-space: -pre-wrap; /* Opera 4-6 */
- white-space: -o-pre-wrap; /* Opera 7 */
- word-wrap: break-word;
-}
-
-.log.debug pre {
- color: #00B9F7;
-}
-
-.log.stdout {
-
-}
-
-.log.stderr pre {
- color: #F03A76;
-}
-
-.log.debug {
- display: none;
-}
-
-.console.debug .log.debug {
- display: block;
-}
-
.error {
margin: var(--gutter) auto;
padding: var(--gutter);
diff --git a/client/test/test-component-results.js b/client/test/test-component-results.js
new file mode 100644
index 0000000..815cc6f
--- /dev/null
+++ b/client/test/test-component-results.js
@@ -0,0 +1,42 @@
+var test = require('tape');
+var results = require('../browser/components/results');
+var raf = require('raf');
+var event = require('synthetic-dom-events');
+var document = require('global/document');
+var hg = require('mercury');
+
+test('results state', function(t) {
+ var state = results();
+
+ t.equal(state.open(), false);
+ t.deepEqual(state.logs(), []);
+ t.end();
+});
+
+// TODO(jasoncampbell): Refactor all the boilerplate here into some simple test
+// helpers and assertion wrappers.
+test('toggle open', function(t) {
+ var div = document.createElement('div');
+ document.body.appendChild(div);
+
+ var state = results();
+ var remove = hg.app(div, state, results.render);
+ var toggle = document.getElementsByClassName('toggle-display')[0];
+
+ t.equal(state.open(), false);
+
+ toggle.dispatchEvent(event('click'));
+
+ raf(function(){
+ var results = document.getElementsByClassName('results')[0];
+
+ t.equal(state.open(), true);
+ t.ok(results.className.match('opened'), 'should have opened class');
+
+ // NOTE: maybe reset state here?
+ document.body.removeChild(div);
+ remove();
+
+ t.end();
+ });
+});