Merge pull request #1243 from weaveworks/1228-hotkey-improvements

hotkey fixes and improvements
This commit is contained in:
Simon
2016-04-11 11:05:03 +02:00
9 changed files with 185 additions and 16 deletions

View File

@@ -13,6 +13,22 @@ import AppStore from '../stores/app-store';
const log = debug('scope:app-actions');
export function showHelp() {
AppDispatcher.dispatch({type: ActionTypes.SHOW_HELP});
}
export function hideHelp() {
AppDispatcher.dispatch({type: ActionTypes.HIDE_HELP});
}
export function toggleHelp() {
if (AppStore.getShowingHelp()) {
hideHelp();
} else {
showHelp();
}
}
export function selectMetric(metricId) {
AppDispatcher.dispatch({
type: ActionTypes.SELECT_METRIC,
@@ -219,7 +235,9 @@ export function enterNode(nodeId) {
export function hitEsc() {
const controlPipe = AppStore.getControlPipe();
if (controlPipe && controlPipe.get('status') === 'PIPE_DELETED') {
if (AppStore.getShowingHelp()) {
hideHelp();
} else if (controlPipe && controlPipe.get('status') === 'PIPE_DELETED') {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_CLOSE_TERMINAL,
pipeId: controlPipe.get('id')

View File

@@ -7,12 +7,13 @@ import Logo from './logo';
import AppStore from '../stores/app-store';
import Footer from './footer.js';
import Sidebar from './sidebar.js';
import HelpPanel from './help-panel';
import Status from './status.js';
import Topologies from './topologies.js';
import TopologyOptions from './topology-options.js';
import { getApiDetails, getTopologies } from '../utils/web-api-utils';
import { pinNextMetric, hitEsc, unpinMetric,
selectMetric } from '../actions/app-actions';
selectMetric, toggleHelp } from '../actions/app-actions';
import Details from './details';
import Nodes from './nodes';
import MetricSelector from './metric-selector';
@@ -22,10 +23,6 @@ import { showingDebugToolbar, toggleDebugToolbar,
DebugToolbar } from './debug-toolbar.js';
const ESC_KEY_CODE = 27;
const D_KEY_CODE = 68;
const Q_KEY_CODE = 81;
const RIGHT_ANGLE_KEY_IDENTIFIER = 'U+003C';
const LEFT_ANGLE_KEY_IDENTIFIER = 'U+003E';
const keyPressLog = debug('scope:app-key-press');
/* make sure these can all be shallow-checked for equality for PureRenderMixin */
@@ -47,6 +44,7 @@ function getStateFromStores() {
availableCanvasMetrics: AppStore.getAvailableCanvasMetrics(),
nodeDetails: AppStore.getNodeDetails(),
nodes: AppStore.getNodes(),
showingHelp: AppStore.getShowingHelp(),
selectedNodeId: AppStore.getSelectedNodeId(),
selectedMetric: AppStore.getSelectedMetric(),
topologies: AppStore.getTopologies(),
@@ -65,12 +63,14 @@ export default class App extends React.Component {
super(props, context);
this.onChange = this.onChange.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.state = getStateFromStores();
}
componentDidMount() {
AppStore.addListener(this.onChange);
window.addEventListener('keyup', this.onKeyPress);
window.addEventListener('keypress', this.onKeyPress);
window.addEventListener('keyup', this.onKeyUp);
getRouter().start({hashbang: true});
if (!AppStore.isRouteSet()) {
@@ -80,24 +80,43 @@ export default class App extends React.Component {
getApiDetails();
}
componentWillUnmount() {
window.removeEventListener('keypress', this.onKeyPress);
window.removeEventListener('keyup', this.onKeyUp);
}
onChange() {
this.setState(getStateFromStores());
}
onKeyPress(ev) {
keyPressLog('onKeyPress', 'keyCode', ev.keyCode, ev);
onKeyUp(ev) {
// don't get esc in onKeyPress
if (ev.keyCode === ESC_KEY_CODE) {
hitEsc();
} else if (ev.keyIdentifier === RIGHT_ANGLE_KEY_IDENTIFIER) {
}
}
onKeyPress(ev) {
//
// keyup gives 'key'
// keypress gives 'char'
// Distinction is important for international keyboard layouts where there
// is often a different {key: char} mapping.
//
keyPressLog('onKeyPress', 'keyCode', ev.keyCode, ev);
const char = String.fromCharCode(ev.charCode);
if (char === '<') {
pinNextMetric(-1);
} else if (ev.keyIdentifier === LEFT_ANGLE_KEY_IDENTIFIER) {
} else if (char === '>') {
pinNextMetric(1);
} else if (ev.keyCode === Q_KEY_CODE) {
} else if (char === 'q') {
unpinMetric();
selectMetric(null);
} else if (ev.keyCode === D_KEY_CODE) {
} else if (char === 'd') {
toggleDebugToolbar();
this.forceUpdate();
} else if (char === '?') {
toggleHelp();
}
}
@@ -112,6 +131,9 @@ export default class App extends React.Component {
return (
<div className="app">
{showingDebugToolbar() && <DebugToolbar />}
{this.state.showingHelp && <HelpPanel />}
{showingDetails && <Details nodes={this.state.nodes}
controlStatus={this.state.controlStatus}
details={this.state.nodeDetails} />}

View File

@@ -4,7 +4,7 @@ import moment from 'moment';
import { getUpdateBufferSize } from '../utils/update-buffer-utils';
import { contrastModeUrl, isContrastMode } from '../utils/contrast-utils';
import { clickDownloadGraph, clickForceRelayout, clickPauseUpdate,
clickResumeUpdate } from '../actions/app-actions';
clickResumeUpdate, toggleHelp } from '../actions/app-actions';
import { basePathSlash } from '../utils/web-api-utils';
export default function Footer(props) {
@@ -64,6 +64,9 @@ export default function Footer(props) {
<a className="footer-icon" href="https://gitreports.com/issue/weaveworks/scope" target="_blank" title="Report an issue">
<span className="fa fa-bug" />
</a>
<a className="footer-icon" onClick={toggleHelp} title="Show help">
<span className="fa fa-question" />
</a>
</div>
</div>

View File

@@ -0,0 +1,44 @@
import React from 'react';
const GENERAL_SHORTCUTS = [
{key: 'esc', label: 'Close active panel'},
{key: '?', label: 'Toggle shortcut menu'},
];
const CANVAS_METRIC_SHORTCUTS = [
{key: '<', label: 'Select and pin previous metric'},
{key: '>', label: 'Select and pin next metric'},
{key: 'q', label: 'Unpin current metric'},
];
function renderShortcuts(cuts) {
return (
<div>
{cuts.map(({key, label}) => (
<div key={key} className="help-panel-shortcut">
<div className="key"><kbd>{key}</kbd></div>
<div className="label">{label}</div>
</div>
))}
</div>
);
}
export default class HelpPanel extends React.Component {
render() {
return (
<div className="help-panel">
<div className="help-panel-header">
<h2>Keyboard Shortcuts</h2>
</div>
<div className="help-panel-main">
<h3>General</h3>
{renderShortcuts(GENERAL_SHORTCUTS)}
<h3>Canvas Metrics</h3>
{renderShortcuts(CANVAS_METRIC_SHORTCUTS)}
</div>
</div>
);
}
}

View File

@@ -175,6 +175,7 @@ export default class Terminal extends React.Component {
if (this.term) {
log('destroy terminal');
this.term.blur();
this.term.destroy();
this.term = null;
}

View File

@@ -21,6 +21,7 @@ const ACTION_TYPES = [
'DO_CONTROL_SUCCESS',
'ENTER_EDGE',
'ENTER_NODE',
'HIDE_HELP',
'LEAVE_EDGE',
'LEAVE_NODE',
'PIN_METRIC',
@@ -36,7 +37,8 @@ const ACTION_TYPES = [
'RECEIVE_API_DETAILS',
'RECEIVE_ERROR',
'ROUTE_TOPOLOGY',
'SELECT_METRIC'
'SELECT_METRIC',
'SHOW_HELP'
];
export default _.zipObject(ACTION_TYPES, ACTION_TYPES);

View File

@@ -58,6 +58,7 @@ let routeSet = false;
let controlPipes = makeOrderedMap(); // pipeId -> controlPipe
let updatePausedAt = null; // Date
let websocketClosed = true;
let showingHelp = false;
let selectedMetric = null;
let pinnedMetric = selectedMetric;
@@ -149,6 +150,10 @@ export class AppStore extends Store {
};
}
getShowingHelp() {
return showingHelp;
}
getActiveTopologyOptions() {
// options for current topology, sub-topologies share options with parent
if (currentTopology && currentTopology.get('parentId')) {
@@ -449,6 +454,16 @@ export class AppStore extends Store {
this.__emitChange();
break;
}
case ActionTypes.SHOW_HELP: {
showingHelp = true;
this.__emitChange();
break;
}
case ActionTypes.HIDE_HELP: {
showingHelp = false;
this.__emitChange();
break;
}
case ActionTypes.DESELECT_NODE: {
closeNodeDetails();
this.__emitChange();

View File

@@ -19,7 +19,7 @@ export function updateRoute() {
const urlStateString = window.location.hash
.replace('#!/state/', '')
.replace('#!/', '') || '{}';
const prevState = JSON.parse(urlStateString);
const prevState = JSON.parse(decodeURIComponent(urlStateString));
if (shouldReplaceState(prevState, state)) {
// Replace the top of the history rather than pushing on a new item.

View File

@@ -1080,6 +1080,70 @@ h2 {
}
}
//
// Help panel!
//
@help-panel-width: 400px;
@help-panel-height: 380px;
.help-panel {
position: absolute;
-webkit-transform: translate3d(0, 0, 0);
top: 50%;
left: 50%;
width: @help-panel-width;
height: @help-panel-height;
margin-left: @help-panel-width / -2;
margin-top: @help-panel-height / -2;
z-index: 2048;
background-color: white;
.shadow-2;
&-header {
background-color: @weave-blue;
padding: 36px;
color: white;
h2 {
margin: 0;
}
}
&-main {
padding: 12px 36px 36px;
}
h3 {
text-transform: uppercase;
font-size: 90%;
color: #8383ac;
padding: 4px 0;
}
&-shortcut {
kbd {
display: inline-block;
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
color: #555;
vertical-align: middle;
background-color: #fcfcfc;
border: solid 1px #ccc;
border-bottom-color: #bbb;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #bbb;
}
div.key {
width: 100px;
display: inline-block;
}
div.label {
display: inline-block;
}
}
}
//
// Debug panel!
//