Files
weave-scope/client/app/scripts/utils/web-api-utils.js
David Kaltschmidt d39fd847b7 Details Panel UI Redesign
Refactored nodedetails to support multiple data sets, probably broke some tests
  Allow api requests to out-of-view topologies
  Fix ESC behavior with details panel
  Stack details panel like cards
  Details pain side-by-side
  Details panel piles
  Fix node details table header styles
  Render load and not-found captions like relatives
  Fix topology click action
  Make node detail tables sortable
  Grouped metrics for details health
  Group metrics in same style
  Link node details children
  Fix scroll issues on double-details
  Fix DESC sort order for node details table
  Save selected node labels in state - allows rendering of node labels from other topologies before details are loaded
  Change detail card UX, newest one at top, pile at bottom
  Details panel one pile w/ animation
  Sort details table nodes by metadata too
  Animate sidepanel from children too
  Fix radial layout
  Dont set origin if a node was already selected, suppresses animation
  stack effect: shift top cards to the left, shrink lower cards vertically
  Clear details card stack if sibling is selected
  Check if node is still selected on API response
  Make detail table sorters robust against non-uniform metadata
  Dont show scrollbar all the time, fix sort icon issue
  Button to show topology for relative
  Overflow metrics for details panel health
  Fix JS error when no metrics are available for container image details
  Column-based rendering of node details table
  Fix JS tests
  Review feedback (UI)
2016-01-19 16:47:05 +01:00

228 lines
5.9 KiB
JavaScript

import debug from 'debug';
import reqwest from 'reqwest';
import { clearControlError, closeWebsocket, openWebsocket, receiveError,
receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError,
receiveControlPipe, receiveControlPipeStatus, receiveControlSuccess,
receiveTopologies, receiveNotFound } from '../actions/app-actions';
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = wsProto + '://' + location.host + location.pathname.replace(/\/$/, '');
const log = debug('scope:web-api-utils');
const apiTimerInterval = 10000;
const reconnectTimerInterval = 5000;
const topologyTimerInterval = apiTimerInterval;
const updateFrequency = '5s';
let socket;
let reconnectTimer = 0;
let currentUrl = null;
let currentOptions = null;
let topologyTimer = 0;
let apiDetailsTimer = 0;
let controlErrorTimer = 0;
function buildOptionsQuery(options) {
if (options) {
return options.reduce(function(query, value, param) {
return `${query}&${param}=${value}`;
}, '');
}
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(/\/$/, '');
}
function createWebsocket(topologyUrl, optionsQuery) {
if (socket) {
socket.onclose = null;
socket.onerror = null;
socket.close();
}
socket = new WebSocket(wsUrl + topologyUrl
+ '/ws?t=' + updateFrequency + '&' + optionsQuery);
socket.onopen = function() {
openWebsocket();
};
socket.onclose = function() {
clearTimeout(reconnectTimer);
socket = null;
closeWebsocket();
log('Closed websocket to ' + topologyUrl);
reconnectTimer = setTimeout(function() {
createWebsocket(topologyUrl, optionsQuery);
}, reconnectTimerInterval);
};
socket.onerror = function() {
log('Error in websocket to ' + topologyUrl);
receiveError(currentUrl);
};
socket.onmessage = function(event) {
const msg = JSON.parse(event.data);
receiveNodesDelta(msg);
};
}
/* keep URLs relative */
export function getTopologies(options) {
clearTimeout(topologyTimer);
const optionsQuery = buildOptionsQuery(options);
const url = `api/topology?${optionsQuery}`;
reqwest({
url: url,
success: function(res) {
receiveTopologies(res);
topologyTimer = setTimeout(function() {
getTopologies(options);
}, topologyTimerInterval / 2);
},
error: function(err) {
log('Error in topology request: ' + err);
receiveError(url);
topologyTimer = setTimeout(function() {
getTopologies(options);
}, topologyTimerInterval / 2);
}
});
}
export function getNodesDelta(topologyUrl, options) {
const optionsQuery = buildOptionsQuery(options);
// only recreate websocket if url changed
if (topologyUrl && (topologyUrl !== currentUrl || currentOptions !== optionsQuery)) {
createWebsocket(topologyUrl, optionsQuery);
currentUrl = topologyUrl;
currentOptions = optionsQuery;
}
}
export function getNodeDetails(topologyUrlsById, nodeMap) {
// get details for all opened nodes
const obj = nodeMap.last();
if (obj && topologyUrlsById.has(obj.topologyId)) {
const topologyUrl = topologyUrlsById.get(obj.topologyId);
const url = [topologyUrl, '/', encodeURIComponent(obj.id)]
.join('').substr(1);
reqwest({
url: url,
success: function(res) {
// make sure node is still selected
if (nodeMap.has(res.node.id)) {
receiveNodeDetails(res.node);
}
},
error: function(err) {
log('Error in node details request: ' + err.responseText);
// dont treat missing node as error
if (err.status === 404) {
receiveNotFound(obj.id);
} else {
receiveError(topologyUrl);
}
}
});
} else {
log('No details or url found for ', obj);
}
}
export function getApiDetails() {
clearTimeout(apiDetailsTimer);
const url = 'api';
reqwest({
url: url,
success: function(res) {
receiveApiDetails(res);
apiDetailsTimer = setTimeout(getApiDetails, apiTimerInterval);
},
error: function(err) {
log('Error in api details request: ' + err);
receiveError(url);
apiDetailsTimer = setTimeout(getApiDetails, apiTimerInterval / 2);
}
});
}
export function doControlRequest(nodeId, control) {
clearTimeout(controlErrorTimer);
const url = `api/control/${encodeURIComponent(control.probeId)}/`
+ `${encodeURIComponent(control.nodeId)}/${control.id}`;
reqwest({
method: 'POST',
url: url,
success: function(res) {
receiveControlSuccess(nodeId);
if (res && res.pipe) {
receiveControlPipe(res.pipe, nodeId, res.raw_tty, true);
}
},
error: function(err) {
receiveControlError(nodeId, err.response);
controlErrorTimer = setTimeout(function() {
clearControlError(nodeId);
}, 10000);
}
});
}
export function deletePipe(pipeId) {
const url = `api/pipe/${encodeURIComponent(pipeId)}`;
reqwest({
method: 'DELETE',
url: url,
success: function() {
log('Closed the pipe!');
},
error: function(err) {
log('Error closing pipe:' + err);
receiveError(url);
}
});
}
export function getPipeStatus(pipeId) {
const url = `/api/pipe/${encodeURIComponent(pipeId)}`;
reqwest({
method: 'GET',
url: url,
success: function(res) {
log('ERROR: expected responses: [400, 404]. Got:', res);
},
error: function(err) {
const status = {
400: 'PIPE_ALIVE',
404: 'PIPE_DELETED'
}[err.status];
if (!status) {
log('Unexpected pipe status:', err.status);
return;
}
receiveControlPipeStatus(pipeId, status);
}
});
}