diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js
index 2094e257ba..1b654a9343 100644
--- a/client/app/scripts/actions/app-actions.js
+++ b/client/app/scripts/actions/app-actions.js
@@ -322,6 +322,14 @@ export function receiveApiDetails(apiDetails) {
});
}
+export function receiveControlNodeRemoved(nodeId) {
+ AppDispatcher.dispatch({
+ type: ActionTypes.RECEIVE_CONTROL_NODE_REMOVED,
+ nodeId
+ });
+ updateRoute();
+}
+
export function receiveControlPipeFromParams(pipeId, rawTty) {
// TODO add nodeId
AppDispatcher.dispatch({
diff --git a/client/app/scripts/components/node-details/node-details-controls.js b/client/app/scripts/components/node-details/node-details-controls.js
index d3028e74d5..703d5f79e1 100644
--- a/client/app/scripts/components/node-details/node-details-controls.js
+++ b/client/app/scripts/components/node-details/node-details-controls.js
@@ -1,4 +1,5 @@
import React from 'react';
+import _ from 'lodash';
import NodeDetailsControlButton from './node-details-control-button';
@@ -17,7 +18,7 @@ export default function NodeDetailsControls({controls, error, nodeId, pending})
{error}
}
- {controls && controls.map(control => )}
{controls && }
diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js
index b6290f2fb5..7731438299 100644
--- a/client/app/scripts/constants/action-types.js
+++ b/client/app/scripts/constants/action-types.js
@@ -27,6 +27,7 @@ const ACTION_TYPES = [
'PIN_METRIC',
'UNPIN_METRIC',
'OPEN_WEBSOCKET',
+ 'RECEIVE_CONTROL_NODE_REMOVED',
'RECEIVE_CONTROL_PIPE',
'RECEIVE_CONTROL_PIPE_STATUS',
'RECEIVE_NODE_DETAILS',
diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js
index b9d15d2dc6..cce23cb647 100644
--- a/client/app/scripts/stores/app-store.js
+++ b/client/app/scripts/stores/app-store.js
@@ -561,6 +561,11 @@ export class AppStore extends Store {
this.__emitChange();
break;
}
+ case ActionTypes.RECEIVE_CONTROL_NODE_REMOVED: {
+ closeNodeDetails(payload.nodeId);
+ this.__emitChange();
+ break;
+ }
case ActionTypes.RECEIVE_CONTROL_PIPE: {
controlPipes = controlPipes.set(payload.pipeId, makeOrderedMap({
id: payload.pipeId,
diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js
index ddd0a5f9fe..885adb271c 100644
--- a/client/app/scripts/utils/web-api-utils.js
+++ b/client/app/scripts/utils/web-api-utils.js
@@ -3,8 +3,8 @@ import reqwest from 'reqwest';
import { clearControlError, closeWebsocket, openWebsocket, receiveError,
receiveApiDetails, receiveNodesDelta, receiveNodeDetails, receiveControlError,
- receiveControlPipe, receiveControlPipeStatus, receiveControlSuccess,
- receiveTopologies, receiveNotFound } from '../actions/app-actions';
+ receiveControlNodeRemoved, receiveControlPipe, receiveControlPipeStatus,
+ receiveControlSuccess, receiveTopologies, receiveNotFound } from '../actions/app-actions';
import { API_INTERVAL, TOPOLOGY_INTERVAL } from '../constants/timer';
@@ -184,8 +184,13 @@ export function doControlRequest(nodeId, control) {
url,
success: (res) => {
receiveControlSuccess(nodeId);
- if (res && res.pipe) {
- receiveControlPipe(res.pipe, nodeId, res.raw_tty, true);
+ if (res) {
+ if (res.pipe) {
+ receiveControlPipe(res.pipe, nodeId, res.raw_tty, true);
+ }
+ if (res.removedNode) {
+ receiveControlNodeRemoved(nodeId);
+ }
}
},
error: (err) => {
diff --git a/common/xfer/controls.go b/common/xfer/controls.go
index 49a0f35add..244d70add2 100644
--- a/common/xfer/controls.go
+++ b/common/xfer/controls.go
@@ -18,10 +18,15 @@ type Request struct {
// Response is the Probe -> App -> UI message type for the control RPCs.
type Response struct {
- Value interface{} `json:"value,omitempty"`
- Error string `json:"error,omitempty"`
- Pipe string `json:"pipe,omitempty"`
- RawTTY bool `json:"raw_tty,omitempty"`
+ Value interface{} `json:"value,omitempty"`
+ Error string `json:"error,omitempty"`
+
+ // Pipe specific fields
+ Pipe string `json:"pipe,omitempty"`
+ RawTTY bool `json:"raw_tty,omitempty"`
+
+ // Remove specific fields
+ RemovedNode string `json:"removedNode,omitempty"` // Set if node was removed
}
// Message is the unions of Request, Response and arbitrary Value.
diff --git a/probe/docker/container.go b/probe/docker/container.go
index ac16d03779..c5387e910f 100644
--- a/probe/docker/container.go
+++ b/probe/docker/container.go
@@ -394,7 +394,7 @@ func (c *container) GetNode(localAddrs []net.IP) report.Node {
RestartContainer, StopContainer, PauseContainer, AttachContainer, ExecContainer,
)
} else {
- result = result.WithControls(StartContainer)
+ result = result.WithControls(StartContainer, RemoveContainer)
}
result = result.AddTable(LabelPrefix, c.container.Config.Labels)
diff --git a/probe/docker/controls.go b/probe/docker/controls.go
index 23b8d23ba2..de2f4fe30f 100644
--- a/probe/docker/controls.go
+++ b/probe/docker/controls.go
@@ -17,6 +17,7 @@ const (
RestartContainer = "docker_restart_container"
PauseContainer = "docker_pause_container"
UnpauseContainer = "docker_unpause_container"
+ RemoveContainer = "docker_remove_container"
AttachContainer = "docker_attach_container"
ExecContainer = "docker_exec_container"
@@ -48,6 +49,18 @@ func (r *registry) unpauseContainer(containerID string, _ xfer.Request) xfer.Res
return xfer.ResponseError(r.client.UnpauseContainer(containerID))
}
+func (r *registry) removeContainer(containerID string, _ xfer.Request) xfer.Response {
+ log.Infof("Removing container %s", containerID)
+ if err := r.client.RemoveContainer(docker_client.RemoveContainerOptions{
+ ID: containerID,
+ }); err != nil {
+ return xfer.ResponseError(err)
+ }
+ return xfer.Response{
+ RemovedNode: containerID,
+ }
+}
+
func (r *registry) attachContainer(containerID string, req xfer.Request) xfer.Response {
c, ok := r.GetContainer(containerID)
if !ok {
@@ -156,6 +169,7 @@ func (r *registry) registerControls() {
controls.Register(RestartContainer, captureContainerID(r.restartContainer))
controls.Register(PauseContainer, captureContainerID(r.pauseContainer))
controls.Register(UnpauseContainer, captureContainerID(r.unpauseContainer))
+ controls.Register(RemoveContainer, captureContainerID(r.removeContainer))
controls.Register(AttachContainer, captureContainerID(r.attachContainer))
controls.Register(ExecContainer, captureContainerID(r.execContainer))
}
@@ -166,6 +180,7 @@ func (r *registry) deregisterControls() {
controls.Rm(RestartContainer)
controls.Rm(PauseContainer)
controls.Rm(UnpauseContainer)
+ controls.Rm(RemoveContainer)
controls.Rm(AttachContainer)
controls.Rm(ExecContainer)
}
diff --git a/probe/docker/registry.go b/probe/docker/registry.go
index 45df659688..1ee0c4ca53 100644
--- a/probe/docker/registry.go
+++ b/probe/docker/registry.go
@@ -70,6 +70,7 @@ type Client interface {
RestartContainer(string, uint) error
PauseContainer(string) error
UnpauseContainer(string) error
+ RemoveContainer(docker_client.RemoveContainerOptions) error
AttachToContainerNonBlocking(docker_client.AttachToContainerOptions) (docker_client.CloseWaiter, error)
CreateExec(docker_client.CreateExecOptions) (*docker_client.Exec, error)
StartExecNonBlocking(string, docker_client.StartExecOptions) (docker_client.CloseWaiter, error)
diff --git a/probe/docker/registry_test.go b/probe/docker/registry_test.go
index 4b5609f200..e3239adcc2 100644
--- a/probe/docker/registry_test.go
+++ b/probe/docker/registry_test.go
@@ -138,6 +138,10 @@ func (m *mockDockerClient) UnpauseContainer(_ string) error {
return fmt.Errorf("unpaused")
}
+func (m *mockDockerClient) RemoveContainer(_ client.RemoveContainerOptions) error {
+ return fmt.Errorf("remove")
+}
+
type mockCloseWaiter struct{}
func (mockCloseWaiter) Close() error { return nil }
diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go
index a0c22e7747..2843e2fff7 100644
--- a/probe/docker/reporter.go
+++ b/probe/docker/reporter.go
@@ -101,39 +101,52 @@ func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology {
WithMetricTemplates(ContainerMetricTemplates).
WithTableTemplates(ContainerTableTemplates)
result.Controls.AddControl(report.Control{
- ID: StopContainer,
- Human: "Stop",
- Icon: "fa-stop",
+ ID: AttachContainer,
+ Human: "Attach",
+ Icon: "fa-desktop",
+ Rank: 1,
+ })
+ result.Controls.AddControl(report.Control{
+ ID: ExecContainer,
+ Human: "Exec shell",
+ Icon: "fa-terminal",
+ Rank: 2,
})
result.Controls.AddControl(report.Control{
ID: StartContainer,
Human: "Start",
Icon: "fa-play",
+ Rank: 3,
})
result.Controls.AddControl(report.Control{
ID: RestartContainer,
Human: "Restart",
Icon: "fa-repeat",
+ Rank: 4,
})
result.Controls.AddControl(report.Control{
ID: PauseContainer,
Human: "Pause",
Icon: "fa-pause",
+ Rank: 5,
})
result.Controls.AddControl(report.Control{
ID: UnpauseContainer,
Human: "Unpause",
Icon: "fa-play",
+ Rank: 6,
})
result.Controls.AddControl(report.Control{
- ID: AttachContainer,
- Human: "Attach",
- Icon: "fa-desktop",
+ ID: StopContainer,
+ Human: "Stop",
+ Icon: "fa-stop",
+ Rank: 7,
})
result.Controls.AddControl(report.Control{
- ID: ExecContainer,
- Human: "Exec shell",
- Icon: "fa-terminal",
+ ID: RemoveContainer,
+ Human: "Remove",
+ Icon: "fa-trash-o",
+ Rank: 8,
})
metadata := map[string]string{report.ControlProbeID: r.probeID}
diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go
index d27a68731b..45d646a02e 100644
--- a/probe/kubernetes/reporter.go
+++ b/probe/kubernetes/reporter.go
@@ -129,6 +129,7 @@ func (r *Reporter) podTopology(services []Service) (report.Topology, report.Topo
ID: GetLogs,
Human: "Get logs",
Icon: "fa-desktop",
+ Rank: 0,
})
for _, service := range services {
selectors[service.ID()] = service.Selector()
diff --git a/render/detailed/node.go b/render/detailed/node.go
index 7ab8de2939..11e110e350 100644
--- a/render/detailed/node.go
+++ b/render/detailed/node.go
@@ -45,6 +45,7 @@ type wiredControlInstance struct {
ID string `json:"id"`
Human string `json:"human"`
Icon string `json:"icon"`
+ Rank int `json:"rank"`
}
// CodecEncodeSelf marshals this ControlInstance. It takes the basic Metric
@@ -56,6 +57,7 @@ func (c *ControlInstance) CodecEncodeSelf(encoder *codec.Encoder) {
ID: c.Control.ID,
Human: c.Control.Human,
Icon: c.Control.Icon,
+ Rank: c.Control.Rank,
})
}
@@ -70,6 +72,7 @@ func (c *ControlInstance) CodecDecodeSelf(decoder *codec.Decoder) {
ID: in.ID,
Human: in.Human,
Icon: in.Icon,
+ Rank: in.Rank,
},
}
}
diff --git a/report/controls.go b/report/controls.go
index e4c3d243b2..4c294aa0ad 100644
--- a/report/controls.go
+++ b/report/controls.go
@@ -16,6 +16,7 @@ type Control struct {
ID string `json:"id"`
Human string `json:"human"`
Icon string `json:"icon"` // from https://fortawesome.github.io/Font-Awesome/cheatsheet/ please
+ Rank int `json:"rank"`
}
// Merge merges other with cs, returning a fresh Controls.