From 3f27086fd0aa7e719aa5dab7f43737a50ac764e4 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 12 Jun 2015 09:41:04 +0200 Subject: [PATCH 1/5] Start on grouping redesign From 816e1e9e995748916e8fed0d8ae2e8de45dbbc75 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 12 Jun 2015 11:37:49 +0200 Subject: [PATCH 2/5] layout of sub-topologies * uses injected static topology, see web-api-utils.js --- client/app/scripts/components/app.js | 2 - client/app/scripts/components/topologies.js | 27 ++++++-- .../stores/__tests__/app-store-test.js | 27 +++++--- client/app/scripts/stores/app-store.js | 24 +++++-- client/app/scripts/utils/web-api-utils.js | 44 ++++++++++++- client/app/styles/main.less | 62 ++++++++----------- 6 files changed, 124 insertions(+), 62 deletions(-) diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index c8a78a080..febbebb16 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -2,7 +2,6 @@ const React = require('react'); const Logo = require('./logo'); const AppStore = require('../stores/app-store'); -const Groupings = require('./groupings.js'); const Status = require('./status.js'); const Topologies = require('./topologies.js'); const WebapiUtils = require('../utils/web-api-utils'); @@ -67,7 +66,6 @@ const App = React.createClass({
-
diff --git a/client/app/scripts/components/topologies.js b/client/app/scripts/components/topologies.js index 152e4245c..d13a9e955 100644 --- a/client/app/scripts/components/topologies.js +++ b/client/app/scripts/components/topologies.js @@ -11,29 +11,46 @@ const Topologies = React.createClass({ AppActions.clickTopology(ev.currentTarget.getAttribute('rel')); }, + renderSubTopology: function(subTopology) { + const isActive = subTopology.name === this.props.currentTopology.name; + const topologyId = AppStore.getTopologyIdForUrl(subTopology.url); + const className = isActive ? 'topologies-sub-item topologies-sub-item-active' : 'topologies-sub-item'; + + return ( +
+
+ {subTopology.name} +
+
+ ); + }, + renderTopology: function(topology) { const isActive = topology.name === this.props.currentTopology.name; - const className = isActive ? 'topologies-item topologies-item-active' : 'topologies-item'; + const className = isActive ? 'topologies-item-main topologies-item-main-active' : 'topologies-item-main'; const topologyId = AppStore.getTopologyIdForUrl(topology.url); const title = ['Topology: ' + topology.name, 'Nodes: ' + topology.stats.node_count, 'Connections: ' + topology.stats.node_count].join('\n'); return ( -
-
+
+
{topology.name}
+
+ {topology.sub_topologies && topology.sub_topologies.map(this.renderSubTopology)} +
); }, render: function() { const topologies = _.sortBy(this.props.topologies, function(topology) { - return topology.name; - }); + return topology.name; + }); return (
diff --git a/client/app/scripts/stores/__tests__/app-store-test.js b/client/app/scripts/stores/__tests__/app-store-test.js index bcca13750..7ca3c6668 100644 --- a/client/app/scripts/stores/__tests__/app-store-test.js +++ b/client/app/scripts/stores/__tests__/app-store-test.js @@ -16,6 +16,11 @@ describe('AppStore', function() { nodeId: 'n1' }; + const ClickSubTopologyAction = { + type: ActionTypes.CLICK_TOPOLOGY, + topologyId: 'topo1-grouped' + }; + const ClickTopologyAction = { type: ActionTypes.CLICK_TOPOLOGY, topologyId: 'topo1' @@ -45,8 +50,11 @@ describe('AppStore', function() { type: ActionTypes.RECEIVE_TOPOLOGIES, topologies: [{ url: '/topo1', - grouped_url: '/topo1grouped', - name: 'Topo1' + name: 'Topo1', + sub_topologies: [{ + url: '/topo1-grouped', + name: 'topo 1 grouped' + }] }] }; @@ -77,14 +85,13 @@ describe('AppStore', function() { expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1'); }); - it('get grouped topology', function() { - registeredCallback(ClickTopologyAction); + it('get sub-topology', function() { registeredCallback(ReceiveTopologiesAction); - registeredCallback(ClickGroupingAction); + registeredCallback(ClickSubTopologyAction); expect(AppStore.getTopologies().length).toBe(1); - expect(AppStore.getCurrentTopology().name).toBe('Topo1'); - expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1grouped'); + expect(AppStore.getCurrentTopology().name).toBe('topo 1 grouped'); + expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1-grouped'); }); // browsing @@ -110,14 +117,14 @@ describe('AppStore', function() { registeredCallback(ReceiveNodesDeltaAction); // TODO clear AppStore cache expect(AppStore.getAppState()) - .toEqual({"topologyId":"topo1","grouping":"grouped","selectedNodeId": null}); + .toEqual({"topologyId":"topo1-grouped","grouping":"none","selectedNodeId": null}); registeredCallback(ClickNodeAction); expect(AppStore.getAppState()) - .toEqual({"topologyId":"topo1","grouping":"grouped","selectedNodeId": 'n1'}); + .toEqual({"topologyId":"topo1-grouped","grouping":"none","selectedNodeId": 'n1'}); // go back in browsing - RouteAction.state = {"topologyId":"topo1","grouping":"grouped","selectedNodeId": null}; + RouteAction.state = {"topologyId":"topo1-grouped","grouping":"none","selectedNodeId": null}; registeredCallback(RouteAction); expect(AppStore.getSelectedNodeId()).toBe(null); expect(AppStore.getNodes()).toEqual(NODE_SET); diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 4670b823d..8db061b74 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -9,8 +9,22 @@ const Naming = require('../constants/naming'); // Helpers -function isUrlForTopologyId(url, topologyId) { - return _.endsWith(url, topologyId); +function findCurrentTopology(subTree, topologyId) { + let foundTopology; + + _.each(subTree, function(topology) { + if (_.endsWith(topology.url, topologyId)) { + foundTopology = topology; + } + if (!foundTopology) { + foundTopology = findCurrentTopology(topology.sub_topologies, topologyId); + } + if (foundTopology) { + return false; + } + }); + + return foundTopology; } // Initial values @@ -45,16 +59,14 @@ const AppStore = assign({}, EventEmitter.prototype, { }, getCurrentTopology: function() { - return _.find(topologies, function(topology) { - return isUrlForTopologyId(topology.url, currentTopologyId); - }); + return findCurrentTopology(topologies, currentTopologyId); }, getCurrentTopologyUrl: function() { const topology = this.getCurrentTopology(); if (topology) { - return topology.grouped_url && currentGrouping === 'grouped' ? topology.grouped_url : topology.url; + return topology.url; } }, diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index dce7160c3..b5d37e851 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -39,10 +39,50 @@ function createWebsocket(topologyUrl) { currentUrl = topologyUrl; } + +const TOPOLOGIES = [ + { + 'name': 'Applications', + 'url': '/api/topology/applications', + 'stats': { + 'node_count': 12, + 'nonpseudo_node_count': 10, + 'edge_count': 13 + }, + 'sub_topologies': [ + { + 'name': 'by name', + 'url': '/api/topology/applications-grouped' + } + ] + }, + { + 'name': 'Containers', + 'url': '/api/topology/containers', + 'grouped_url': '/api/topology/containers-grouped', + 'stats': { + 'node_count': 2, + 'nonpseudo_node_count': 1, + 'edge_count': 2 + } + }, + { + 'name': 'Hosts', + 'url': '/api/topology/hosts', + 'stats': { + 'node_count': 2, + 'nonpseudo_node_count': 1, + 'edge_count': 2 + } + } +]; + + function getTopologies() { clearTimeout(topologyTimer); - reqwest('/api/topology', function(res) { - AppActions.receiveTopologies(res); + reqwest('/api/topology', function() { + // injecting static topos + AppActions.receiveTopologies(TOPOLOGIES); topologyTimer = setTimeout(getTopologies, 10000); }); } diff --git a/client/app/styles/main.less b/client/app/styles/main.less index b4df69bf9..fb3f342cd 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -81,59 +81,47 @@ body { float: left; } -.topologies, -.groupings { - float: left; - margin-top: 7px; - margin-left: 48px; -} - .topologies { - &-icon { - font-size: 12px; - color: @text-secondary-color; - margin-right: 16px; - position: relative; - top: -1px; - } + float: left; + margin: 4px 64px; .topologies-item { - margin: 8px 16px 6px 0; - cursor: pointer; - display: inline-block; + margin: 0px 16px; + float: left; &-label { color: @text-secondary-color; - font-size: 15px; + font-size: 16px; text-transform: uppercase; } + } + + .topologies-sub { + margin-top: 4px; + + &-item { + &-label { + color: @text-secondary-color; + font-size: 12px; + text-transform: uppercase; + } + } + } + + .topologies-item-main, + .topologies-sub-item { + cursor: pointer; + &-active, &:hover { + .topologies-sub-item-label, .topologies-item-label { color: @text-color; } } + } -} -.groupings { - &-item { - font-size: 15px; - margin: 8px 12px 6px 0; - cursor: pointer; - display: inline-block; - color: @text-tertiary-color; - - &-disabled { - color: @text-tertiary-color; - cursor: default; - } - - &-default:hover, - &-active { - color: @text-color; - } - } } .status { From 086ff3b791979d39105d902b2d854bd927d0334d Mon Sep 17 00:00:00 2001 From: Peter Bourgon Date: Fri, 12 Jun 2015 13:23:25 +0200 Subject: [PATCH 3/5] Change from grouped URL to sub-topologies --- app/api_topologies.go | 45 ++++++++++++++---------------- app/api_topologies_test.go | 26 +++++++++++------- app/main.go | 2 +- app/router.go | 56 ++++++++++++++++++++++++++++---------- 4 files changed, 80 insertions(+), 49 deletions(-) diff --git a/app/api_topologies.go b/app/api_topologies.go index 70b9636cb..4684b2231 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -2,17 +2,16 @@ package main import ( "net/http" - "strings" "github.com/weaveworks/scope/report" ) // APITopologyDesc is returned in a list by the /api/topology handler. type APITopologyDesc struct { - Name string `json:"name"` - URL string `json:"url"` - GroupedURL string `json:"grouped_url,omitempty"` - Stats topologyStats `json:"stats"` + Name string `json:"name"` + URL string `json:"url"` + SubTopologies []APITopologyDesc `json:"sub_topologies,omitempty"` + Stats *topologyStats `json:"stats,omitempty"` } type topologyStats struct { @@ -25,31 +24,29 @@ type topologyStats struct { func makeTopologyList(rep Reporter) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { rpt := rep.Report() - - var a []APITopologyDesc + topologies := []APITopologyDesc{} for name, def := range topologyRegistry { - if strings.HasSuffix(name, "grouped") { - continue + subTopologies := []APITopologyDesc{} + for subName, subDef := range topologyRegistry { + if subDef.parent == name { + subTopologies = append(subTopologies, APITopologyDesc{ + Name: subDef.human, + URL: "/api/topology/" + subName, + }) + } } - - url := "/api/topology/" + name - var groupedURL string - if def.groupedTopology != "" { - groupedURL = "/api/topology/" + def.groupedTopology - } - - a = append(a, APITopologyDesc{ - Name: def.human, - URL: url, - GroupedURL: groupedURL, - Stats: stats(render(rpt, def.maps)), + topologies = append(topologies, APITopologyDesc{ + Name: def.human, + URL: "/api/topology/" + name, + SubTopologies: subTopologies, + Stats: stats(render(rpt, def.maps)), }) } - respondWith(w, http.StatusOK, a) + respondWith(w, http.StatusOK, topologies) } } -func stats(r report.RenderableNodes) topologyStats { +func stats(r report.RenderableNodes) *topologyStats { var ( nodes int realNodes int @@ -64,7 +61,7 @@ func stats(r report.RenderableNodes) topologyStats { edges += len(n.Adjacency) } - return topologyStats{ + return &topologyStats{ NodeCount: nodes, NonpseudoNodeCount: realNodes, EdgeCount: edges, diff --git a/app/api_topologies_test.go b/app/api_topologies_test.go index 0c9b67da4..7009c6542 100644 --- a/app/api_topologies_test.go +++ b/app/api_topologies_test.go @@ -11,23 +11,29 @@ func TestAPITopology(t *testing.T) { defer ts.Close() body := getRawJSON(t, ts, "/api/topology") - var topos []APITopologyDesc - if err := json.Unmarshal(body, &topos); err != nil { + + var topologies []APITopologyDesc + if err := json.Unmarshal(body, &topologies); err != nil { t.Fatalf("JSON parse error: %s", err) } - equals(t, 3, len(topos)) - for _, topo := range topos { - is200(t, ts, topo.URL) - if topo.GroupedURL != "" { - is200(t, ts, topo.GroupedURL) + equals(t, 5, len(topologies)) + + for _, topology := range topologies { + is200(t, ts, topology.URL) + + for _, subTopology := range topology.SubTopologies { + is200(t, ts, subTopology.URL) } - if have := topo.Stats.EdgeCount; have <= 0 { + + if have := topology.Stats.EdgeCount; have <= 0 { t.Errorf("EdgeCount isn't positive: %d", have) } - if have := topo.Stats.NodeCount; have <= 0 { + + if have := topology.Stats.NodeCount; have <= 0 { t.Errorf("NodeCount isn't positive: %d", have) } - if have := topo.Stats.NonpseudoNodeCount; have <= 0 { + + if have := topology.Stats.NonpseudoNodeCount; have <= 0 { t.Errorf("NonpseudoNodeCount isn't positive: %d", have) } } diff --git a/app/main.go b/app/main.go index 10b3f4196..a43ed3b8c 100644 --- a/app/main.go +++ b/app/main.go @@ -18,7 +18,7 @@ import ( ) // Set during buildtime. -var version = "unknown" +var version = "dev" func main() { var ( diff --git a/app/router.go b/app/router.go index ff4fa7b56..7bd30f178 100644 --- a/app/router.go +++ b/app/router.go @@ -45,10 +45,49 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { respondWith(w, http.StatusOK, APIDetails{Version: version}) } +var topologyRegistry = map[string]topologyView{ + "applications": { + human: "Applications", + parent: "", + maps: []topologyMapper{ + {report.SelectEndpoint, report.ProcessPID, report.GenericPseudoNode}, + }, + }, + "applications-by-name": { + human: "Applications by name", + parent: "applications", + maps: []topologyMapper{ + {report.SelectEndpoint, report.ProcessName, report.GenericGroupedPseudoNode}, + }, + }, + "containers": { + human: "Containers", + parent: "", + maps: []topologyMapper{ + {report.SelectEndpoint, report.MapEndpoint2Container, report.InternetOnlyPseudoNode}, + {report.SelectContainer, report.MapContainerIdentity, report.InternetOnlyPseudoNode}, + }, + }, + "containers-by-image": { + human: "Containers by image", + parent: "containers", + maps: []topologyMapper{ + {report.SelectEndpoint, report.ProcessContainerImage, report.InternetOnlyPseudoNode}, + }, + }, + "hosts": { + human: "Hosts", + parent: "", + maps: []topologyMapper{ + {report.SelectAddress, report.NetworkHostname, report.GenericPseudoNode}, + }, + }, +} + type topologyView struct { - human string - groupedTopology string - maps []topologyMapper + human string + parent string + maps []topologyMapper } type topologyMapper struct { @@ -56,14 +95,3 @@ type topologyMapper struct { mapper report.MapFunc pseudo report.PseudoFunc } - -var topologyRegistry = map[string]topologyView{ - "applications": {"Applications", "applications-grouped", []topologyMapper{{report.SelectEndpoint, report.ProcessPID, report.GenericPseudoNode}}}, - "applications-grouped": {"Applications", "", []topologyMapper{{report.SelectEndpoint, report.ProcessName, report.GenericGroupedPseudoNode}}}, - "containers": {"Containers", "containers-grouped", []topologyMapper{ - {report.SelectEndpoint, report.MapEndpoint2Container, report.InternetOnlyPseudoNode}, - {report.SelectContainer, report.MapContainerIdentity, report.InternetOnlyPseudoNode}, - }}, - "containers-grouped": {"Containers", "", []topologyMapper{{report.SelectEndpoint, report.ProcessContainerImage, report.InternetOnlyPseudoNode}}}, - "hosts": {"Hosts", "", []topologyMapper{{report.SelectAddress, report.NetworkHostname, report.GenericPseudoNode}}}, -} From 3e8bdcc854473a130ecf9542f344758bd0be376b Mon Sep 17 00:00:00 2001 From: Peter Bourgon Date: Mon, 15 Jun 2015 10:26:22 +0200 Subject: [PATCH 4/5] Sub-topolgies rendered in one place only --- app/api_topologies.go | 14 ++++++++++---- app/api_topologies_test.go | 2 +- app/router.go | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/api_topologies.go b/app/api_topologies.go index 4684b2231..f7ac4157b 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -23,15 +23,21 @@ type topologyStats struct { // makeTopologyList returns a handler that yields an APITopologyList. func makeTopologyList(rep Reporter) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - rpt := rep.Report() - topologies := []APITopologyDesc{} + var ( + rpt = rep.Report() + topologies = []APITopologyDesc{} + ) for name, def := range topologyRegistry { + if def.parent != "" { + continue // subtopology, don't show at top level + } subTopologies := []APITopologyDesc{} for subName, subDef := range topologyRegistry { if subDef.parent == name { subTopologies = append(subTopologies, APITopologyDesc{ - Name: subDef.human, - URL: "/api/topology/" + subName, + Name: subDef.human, + URL: "/api/topology/" + subName, + Stats: stats(render(rpt, subDef.maps)), }) } } diff --git a/app/api_topologies_test.go b/app/api_topologies_test.go index 7009c6542..6b5aff968 100644 --- a/app/api_topologies_test.go +++ b/app/api_topologies_test.go @@ -16,7 +16,7 @@ func TestAPITopology(t *testing.T) { if err := json.Unmarshal(body, &topologies); err != nil { t.Fatalf("JSON parse error: %s", err) } - equals(t, 5, len(topologies)) + equals(t, 3, len(topologies)) for _, topology := range topologies { is200(t, ts, topology.URL) diff --git a/app/router.go b/app/router.go index 7bd30f178..4616eb8bc 100644 --- a/app/router.go +++ b/app/router.go @@ -54,7 +54,7 @@ var topologyRegistry = map[string]topologyView{ }, }, "applications-by-name": { - human: "Applications by name", + human: "by name", parent: "applications", maps: []topologyMapper{ {report.SelectEndpoint, report.ProcessName, report.GenericGroupedPseudoNode}, @@ -69,7 +69,7 @@ var topologyRegistry = map[string]topologyView{ }, }, "containers-by-image": { - human: "Containers by image", + human: "by image", parent: "containers", maps: []topologyMapper{ {report.SelectEndpoint, report.ProcessContainerImage, report.InternetOnlyPseudoNode}, From cdccd5d2c0c9efd25d0c02c920bab2fa24eea075 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 15 Jun 2015 10:37:24 +0200 Subject: [PATCH 5/5] render grouped topologies menu sent by backend --- client/app/scripts/components/topologies.js | 13 ++++-- client/app/scripts/utils/web-api-utils.js | 44 +-------------------- 2 files changed, 11 insertions(+), 46 deletions(-) diff --git a/client/app/scripts/components/topologies.js b/client/app/scripts/components/topologies.js index d13a9e955..52abd307d 100644 --- a/client/app/scripts/components/topologies.js +++ b/client/app/scripts/components/topologies.js @@ -14,10 +14,12 @@ const Topologies = React.createClass({ renderSubTopology: function(subTopology) { const isActive = subTopology.name === this.props.currentTopology.name; const topologyId = AppStore.getTopologyIdForUrl(subTopology.url); + const title = this.renderTitle(subTopology); const className = isActive ? 'topologies-sub-item topologies-sub-item-active' : 'topologies-sub-item'; return ( -
+
{subTopology.name}
@@ -25,13 +27,16 @@ const Topologies = React.createClass({ ); }, + renderTitle: function(topology) { + return ['Nodes: ' + topology.stats.node_count, + 'Connections: ' + topology.stats.node_count].join('\n'); + }, + renderTopology: function(topology) { const isActive = topology.name === this.props.currentTopology.name; const className = isActive ? 'topologies-item-main topologies-item-main-active' : 'topologies-item-main'; const topologyId = AppStore.getTopologyIdForUrl(topology.url); - const title = ['Topology: ' + topology.name, - 'Nodes: ' + topology.stats.node_count, - 'Connections: ' + topology.stats.node_count].join('\n'); + const title = this.renderTitle(topology); return (
diff --git a/client/app/scripts/utils/web-api-utils.js b/client/app/scripts/utils/web-api-utils.js index b5d37e851..dce7160c3 100644 --- a/client/app/scripts/utils/web-api-utils.js +++ b/client/app/scripts/utils/web-api-utils.js @@ -39,50 +39,10 @@ function createWebsocket(topologyUrl) { currentUrl = topologyUrl; } - -const TOPOLOGIES = [ - { - 'name': 'Applications', - 'url': '/api/topology/applications', - 'stats': { - 'node_count': 12, - 'nonpseudo_node_count': 10, - 'edge_count': 13 - }, - 'sub_topologies': [ - { - 'name': 'by name', - 'url': '/api/topology/applications-grouped' - } - ] - }, - { - 'name': 'Containers', - 'url': '/api/topology/containers', - 'grouped_url': '/api/topology/containers-grouped', - 'stats': { - 'node_count': 2, - 'nonpseudo_node_count': 1, - 'edge_count': 2 - } - }, - { - 'name': 'Hosts', - 'url': '/api/topology/hosts', - 'stats': { - 'node_count': 2, - 'nonpseudo_node_count': 1, - 'edge_count': 2 - } - } -]; - - function getTopologies() { clearTimeout(topologyTimer); - reqwest('/api/topology', function() { - // injecting static topos - AppActions.receiveTopologies(TOPOLOGIES); + reqwest('/api/topology', function(res) { + AppActions.receiveTopologies(res); topologyTimer = setTimeout(getTopologies, 10000); }); }