mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 02:00:43 +00:00
- Fix text selection in terminal (was our force focus stuff, introduced if you clicked on the black border that was not actually the terminal
307 lines
8.5 KiB
JavaScript
307 lines
8.5 KiB
JavaScript
import debug from 'debug';
|
|
import reqwest from 'reqwest';
|
|
|
|
import { blurSearch, clearControlError, closeWebsocket, openWebsocket, receiveError,
|
|
receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError,
|
|
receiveControlNodeRemoved, receiveControlPipe, receiveControlPipeStatus,
|
|
receiveControlSuccess, receiveTopologies, receiveNotFound,
|
|
receiveNodesForTopology } from '../actions/app-actions';
|
|
|
|
import { API_INTERVAL, TOPOLOGY_INTERVAL } from '../constants/timer';
|
|
|
|
const log = debug('scope:web-api-utils');
|
|
|
|
const reconnectTimerInterval = 5000;
|
|
const updateFrequency = '5s';
|
|
const FIRST_RENDER_TOO_LONG_THRESHOLD = 100; // ms
|
|
|
|
let socket;
|
|
let reconnectTimer = 0;
|
|
let currentUrl = null;
|
|
let currentOptions = null;
|
|
let topologyTimer = 0;
|
|
let apiDetailsTimer = 0;
|
|
let controlErrorTimer = 0;
|
|
let createWebsocketAt = 0;
|
|
let firstMessageOnWebsocketAt = 0;
|
|
|
|
export function buildOptionsQuery(options) {
|
|
if (options) {
|
|
return options.map((value, param) => `${param}=${value}`).join('&');
|
|
}
|
|
return '';
|
|
}
|
|
|
|
export function basePath(urlPath) {
|
|
//
|
|
// "/scope/terminal.html" -> "/scope"
|
|
// "/scope/" -> "/scope"
|
|
// "/scope" -> "/scope"
|
|
// "/" -> ""
|
|
//
|
|
const parts = urlPath.split('/');
|
|
// if the last item has a "." in it, e.g. foo.html...
|
|
if (parts[parts.length - 1].indexOf('.') !== -1) {
|
|
return parts.slice(0, -1).join('/');
|
|
}
|
|
return parts.join('/').replace(/\/$/, '');
|
|
}
|
|
|
|
export function basePathSlash(urlPath) {
|
|
//
|
|
// "/scope/terminal.html" -> "/scope/"
|
|
// "/scope/" -> "/scope/"
|
|
// "/scope" -> "/scope/"
|
|
// "/" -> "/"
|
|
//
|
|
return `${basePath(urlPath)}/`;
|
|
}
|
|
|
|
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
const wsUrl = `${wsProto}://${location.host}${basePath(location.pathname)}`;
|
|
|
|
function createWebsocket(topologyUrl, optionsQuery, dispatch) {
|
|
if (socket) {
|
|
socket.onclose = null;
|
|
socket.onerror = null;
|
|
socket.close();
|
|
// onclose() is not called, but that's fine since we're opening a new one
|
|
// right away
|
|
}
|
|
|
|
// profiling
|
|
createWebsocketAt = new Date();
|
|
firstMessageOnWebsocketAt = 0;
|
|
|
|
socket = new WebSocket(`${wsUrl}${topologyUrl}/ws?t=${updateFrequency}&${optionsQuery}`);
|
|
|
|
socket.onopen = () => {
|
|
dispatch(openWebsocket());
|
|
};
|
|
|
|
socket.onclose = () => {
|
|
clearTimeout(reconnectTimer);
|
|
log(`Closing websocket to ${topologyUrl}`, socket.readyState);
|
|
socket = null;
|
|
dispatch(closeWebsocket());
|
|
|
|
reconnectTimer = setTimeout(() => {
|
|
createWebsocket(topologyUrl, optionsQuery, dispatch);
|
|
}, reconnectTimerInterval);
|
|
};
|
|
|
|
socket.onerror = () => {
|
|
log(`Error in websocket to ${topologyUrl}`);
|
|
dispatch(receiveError(currentUrl));
|
|
};
|
|
|
|
socket.onmessage = (event) => {
|
|
const msg = JSON.parse(event.data);
|
|
dispatch(receiveNodesDelta(msg));
|
|
|
|
// profiling (receiveNodesDelta triggers synchronous render)
|
|
if (!firstMessageOnWebsocketAt) {
|
|
firstMessageOnWebsocketAt = new Date();
|
|
const timeToFirstMessage = firstMessageOnWebsocketAt - createWebsocketAt;
|
|
if (timeToFirstMessage > FIRST_RENDER_TOO_LONG_THRESHOLD) {
|
|
log('Time (ms) to first nodes render after websocket was created',
|
|
firstMessageOnWebsocketAt - createWebsocketAt);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/* keep URLs relative */
|
|
|
|
/**
|
|
* Gets nodes for all topologies (for search)
|
|
*/
|
|
export function getAllNodes(getState, dispatch) {
|
|
const state = getState();
|
|
const topologyOptions = state.get('topologyOptions');
|
|
// fetch sequentially
|
|
state.get('topologyUrlsById')
|
|
.reduce((sequence, topologyUrl, topologyId) => sequence.then(() => {
|
|
const optionsQuery = buildOptionsQuery(topologyOptions.get(topologyId));
|
|
return fetch(`${topologyUrl}?${optionsQuery}`);
|
|
})
|
|
.then(response => response.json())
|
|
.then(json => dispatch(receiveNodesForTopology(json.nodes, topologyId))),
|
|
Promise.resolve());
|
|
}
|
|
|
|
export function getTopologies(options, dispatch) {
|
|
clearTimeout(topologyTimer);
|
|
const optionsQuery = buildOptionsQuery(options);
|
|
const url = `api/topology?${optionsQuery}`;
|
|
reqwest({
|
|
url,
|
|
success: (res) => {
|
|
dispatch(receiveTopologies(res));
|
|
topologyTimer = setTimeout(() => {
|
|
getTopologies(options, dispatch);
|
|
}, TOPOLOGY_INTERVAL);
|
|
},
|
|
error: (err) => {
|
|
log(`Error in topology request: ${err.responseText}`);
|
|
dispatch(receiveError(url));
|
|
topologyTimer = setTimeout(() => {
|
|
getTopologies(options, dispatch);
|
|
}, TOPOLOGY_INTERVAL);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function getNodesDelta(topologyUrl, options, dispatch) {
|
|
const optionsQuery = buildOptionsQuery(options);
|
|
|
|
// only recreate websocket if url changed
|
|
if (topologyUrl && (topologyUrl !== currentUrl || currentOptions !== optionsQuery)) {
|
|
createWebsocket(topologyUrl, optionsQuery, dispatch);
|
|
currentUrl = topologyUrl;
|
|
currentOptions = optionsQuery;
|
|
}
|
|
}
|
|
|
|
export function getNodeDetails(topologyUrlsById, currentTopologyId, options, nodeMap, dispatch) {
|
|
// get details for all opened nodes
|
|
const obj = nodeMap.last();
|
|
if (obj && topologyUrlsById.has(obj.topologyId)) {
|
|
const topologyUrl = topologyUrlsById.get(obj.topologyId);
|
|
let urlComponents = [topologyUrl, '/', encodeURIComponent(obj.id)];
|
|
if (currentTopologyId === obj.topologyId) {
|
|
// Only forward filters for nodes in the current topology
|
|
const optionsQuery = buildOptionsQuery(options);
|
|
urlComponents = urlComponents.concat(['?', optionsQuery]);
|
|
}
|
|
const url = urlComponents.join('').substr(1);
|
|
|
|
reqwest({
|
|
url,
|
|
success: (res) => {
|
|
// make sure node is still selected
|
|
if (nodeMap.has(res.node.id)) {
|
|
dispatch(receiveNodeDetails(res.node));
|
|
}
|
|
},
|
|
error: (err) => {
|
|
log(`Error in node details request: ${err.responseText}`);
|
|
// dont treat missing node as error
|
|
if (err.status === 404) {
|
|
dispatch(receiveNotFound(obj.id));
|
|
} else {
|
|
dispatch(receiveError(topologyUrl));
|
|
}
|
|
}
|
|
});
|
|
} else if (obj) {
|
|
log('No details or url found for ', obj);
|
|
}
|
|
}
|
|
|
|
export function getApiDetails(dispatch) {
|
|
clearTimeout(apiDetailsTimer);
|
|
const url = 'api';
|
|
reqwest({
|
|
url,
|
|
success: (res) => {
|
|
dispatch(receiveApiDetails(res));
|
|
apiDetailsTimer = setTimeout(() => {
|
|
getApiDetails(dispatch);
|
|
}, API_INTERVAL);
|
|
},
|
|
error: (err) => {
|
|
log(`Error in api details request: ${err.responseText}`);
|
|
receiveError(url);
|
|
apiDetailsTimer = setTimeout(() => {
|
|
getApiDetails(dispatch);
|
|
}, API_INTERVAL / 2);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function doControlRequest(nodeId, control, dispatch) {
|
|
clearTimeout(controlErrorTimer);
|
|
const url = `api/control/${encodeURIComponent(control.probeId)}/`
|
|
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
|
|
reqwest({
|
|
method: 'POST',
|
|
url,
|
|
success: (res) => {
|
|
dispatch(receiveControlSuccess(nodeId));
|
|
if (res) {
|
|
if (res.pipe) {
|
|
dispatch(blurSearch());
|
|
dispatch(receiveControlPipe(
|
|
res.pipe,
|
|
nodeId,
|
|
res.raw_tty,
|
|
{id: res.resize_tty_control, probeId: control.probeId, nodeId: control.nodeId}));
|
|
}
|
|
if (res.removedNode) {
|
|
dispatch(receiveControlNodeRemoved(nodeId));
|
|
}
|
|
}
|
|
},
|
|
error: (err) => {
|
|
dispatch(receiveControlError(nodeId, err.response));
|
|
controlErrorTimer = setTimeout(() => {
|
|
dispatch(clearControlError(nodeId));
|
|
}, 10000);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
export function doResizeTty(pipeId, control, cols, rows) {
|
|
const url = `api/control/${encodeURIComponent(control.probeId)}/`
|
|
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
|
|
|
|
return reqwest({
|
|
method: 'POST',
|
|
url,
|
|
data: JSON.stringify({pipeID: pipeId, width: cols.toString(), height: rows.toString()}),
|
|
})
|
|
.fail((err) => {
|
|
log(`Error resizing pipe: ${err}`);
|
|
});
|
|
}
|
|
|
|
|
|
export function deletePipe(pipeId, dispatch) {
|
|
const url = `api/pipe/${encodeURIComponent(pipeId)}`;
|
|
reqwest({
|
|
method: 'DELETE',
|
|
url,
|
|
success: () => {
|
|
log('Closed the pipe!');
|
|
},
|
|
error: (err) => {
|
|
log(`Error closing pipe:${err}`);
|
|
dispatch(receiveError(url));
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
export function getPipeStatus(pipeId, dispatch) {
|
|
const url = `api/pipe/${encodeURIComponent(pipeId)}/check`;
|
|
reqwest({
|
|
method: 'GET',
|
|
url,
|
|
complete: (res) => {
|
|
const status = {
|
|
204: 'PIPE_ALIVE',
|
|
404: 'PIPE_DELETED'
|
|
}[res.status];
|
|
|
|
if (!status) {
|
|
log('Unexpected pipe status:', res.status);
|
|
return;
|
|
}
|
|
|
|
dispatch(receiveControlPipeStatus(pipeId, status));
|
|
}
|
|
});
|
|
}
|