mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 18:20:27 +00:00
Merge pull request #1243 from weaveworks/1228-hotkey-improvements
hotkey fixes and improvements
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
44
client/app/scripts/components/help-panel.js
Normal file
44
client/app/scripts/components/help-panel.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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!
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user