diff --git a/.gitignore b/.gitignore
index ea1975cedc..766b018a54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ _obj
_test
.vagrant
releases
+tmp
# Architecture specific extensions/prefixes
*.[568vq]
diff --git a/client/app/scripts/components/node-details/__tests__/node-details-table-test.js b/client/app/scripts/components/node-details/__tests__/node-details-table-test.js
new file mode 100644
index 0000000000..2aee7c1864
--- /dev/null
+++ b/client/app/scripts/components/node-details/__tests__/node-details-table-test.js
@@ -0,0 +1,106 @@
+import React from 'react';
+import TestUtils from 'react/lib/ReactTestUtils';
+import { Provider } from 'react-redux';
+import configureStore from '../../../stores/configureStore';
+
+// need ES5 require to keep automocking off
+const NodeDetailsTable = require('../node-details-table.js').default;
+
+describe('NodeDetailsTable', () => {
+ let nodes, columns, component;
+
+ beforeEach(() => {
+ columns = [
+ { id: 'kubernetes_ip', label: 'IP', dataType: 'ip' },
+ { id: 'kubernetes_namespace', label: 'Namespace' },
+ ];
+ nodes = [
+ {
+ id: 'node-1',
+ metadata: [
+ { id: 'kubernetes_ip', label: 'IP', value: '10.244.253.24' },
+ { id: 'kubernetes_namespace', label: 'Namespace', value: '1111' },
+ ]
+ }, {
+ id: 'node-2',
+ metadata: [
+ { id: 'kubernetes_ip', label: 'IP', value: '10.244.253.4' },
+ { id: 'kubernetes_namespace', label: 'Namespace', value: '12' },
+ ]
+ }, {
+ id: 'node-3',
+ metadata: [
+ { id: 'kubernetes_ip', label: 'IP', value: '10.44.253.255' },
+ { id: 'kubernetes_namespace', label: 'Namespace', value: '5' },
+ ]
+ }, {
+ id: 'node-4',
+ metadata: [
+ { id: 'kubernetes_ip', label: 'IP', value: '10.244.253.100' },
+ { id: 'kubernetes_namespace', label: 'Namespace', value: '00000' },
+ ]
+ },
+ ];
+ });
+
+ function matchColumnValues(columnLabel, expectedValues) {
+ // Get the index of the column whose values we want to match.
+ const columnIndex = columns.findIndex(column => column.id === columnLabel);
+ // Get all the values rendered in the table.
+ const values = TestUtils.scryRenderedDOMComponentsWithClass(component, 'node-details-table-node-value').map(d => d.title);
+ // Since we are interested only in the values that appear in the column `columnIndex`, we drop the rest.
+ // As `values` are ordered by appearance in the DOM structure (that is, first by row and then by column),
+ // the indexes we are interested in are of the form columnIndex + n * columns.length, where n >= 0.
+ // Therefore we take only the values at the index which divided by columns.length gives a reminder columnIndex.
+ const filteredValues = values.filter((element, index) => index % columns.length === columnIndex);
+ // Array comparison
+ expect(filteredValues).toEqual(expectedValues);
+ }
+
+ function clickColumn(title) {
+ const node = TestUtils.scryRenderedDOMComponentsWithTag(component, 'td').find(d => d.title === title);
+ TestUtils.Simulate.click(node);
+ }
+
+ describe('kubernetes_ip', () => {
+ it('sorts by column', () => {
+ component = TestUtils.renderIntoDocument(
+
+
+
+ );
+
+ matchColumnValues('kubernetes_ip', ['10.44.253.255', '10.244.253.4', '10.244.253.24', '10.244.253.100']);
+ clickColumn('IP');
+ matchColumnValues('kubernetes_ip', ['10.244.253.100', '10.244.253.24', '10.244.253.4', '10.44.253.255']);
+ clickColumn('IP');
+ matchColumnValues('kubernetes_ip', ['10.44.253.255', '10.244.253.4', '10.244.253.24', '10.244.253.100']);
+ });
+ });
+
+ describe('kubernetes_namespace', () => {
+ it('sorts by column', () => {
+ component = TestUtils.renderIntoDocument(
+
+
+
+ );
+
+ matchColumnValues('kubernetes_namespace', ['00000', '1111', '12', '5']);
+ clickColumn('Namespace');
+ matchColumnValues('kubernetes_namespace', ['5', '12', '1111', '00000']);
+ clickColumn('Namespace');
+ matchColumnValues('kubernetes_namespace', ['00000', '1111', '12', '5']);
+ });
+ });
+});
diff --git a/client/app/scripts/components/node-details/node-details-table.js b/client/app/scripts/components/node-details/node-details-table.js
index 9d5d5a0c77..5a441c46fd 100644
--- a/client/app/scripts/components/node-details/node-details-table.js
+++ b/client/app/scripts/components/node-details/node-details-table.js
@@ -4,12 +4,19 @@ import classNames from 'classnames';
import ShowMore from '../show-more';
import NodeDetailsTableRow from './node-details-table-row';
+import { ipToPaddedString } from '../../utils/string-utils';
function isNumber(data) {
return data.dataType && data.dataType === 'number';
}
+
+function isIP(data) {
+ return data.dataType && data.dataType === 'ip';
+}
+
+
const CW = {
XS: '32px',
S: '50px',
@@ -80,7 +87,10 @@ function getNodeValue(node, header) {
let field = _.union(node.metrics, node.metadata).find(f => f.id === fieldId);
if (field) {
- if (isNumber(header)) {
+ if (isIP(header)) {
+ // Format the IPs so that they are sorted numerically.
+ return ipToPaddedString(field.value);
+ } else if (isNumber(header)) {
return parseFloat(field.value);
}
return field.value;
diff --git a/client/app/scripts/utils/__tests__/string-utils-test.js b/client/app/scripts/utils/__tests__/string-utils-test.js
index 8d0ae43cc0..51d6e18414 100644
--- a/client/app/scripts/utils/__tests__/string-utils-test.js
+++ b/client/app/scripts/utils/__tests__/string-utils-test.js
@@ -3,21 +3,30 @@ describe('StringUtils', () => {
const StringUtils = require('../string-utils');
describe('formatMetric', () => {
- const formatMetric = StringUtils.formatMetric;
+ const f = StringUtils.formatMetric;
it('it should render 0', () => {
- expect(formatMetric(0)).toBe('0.00');
+ expect(f(0)).toBe('0.00');
});
});
describe('longestCommonPrefix', () => {
- const fun = StringUtils.longestCommonPrefix;
+ const f = StringUtils.longestCommonPrefix;
it('it should return the longest common prefix', () => {
- expect(fun(['interspecies', 'interstellar'])).toBe('inters');
- expect(fun(['space', 'space'])).toBe('space');
- expect(fun([''])).toBe('');
- expect(fun(['prefix', 'suffix'])).toBe('');
+ expect(f(['interspecies', 'interstellar'])).toBe('inters');
+ expect(f(['space', 'space'])).toBe('space');
+ expect(f([''])).toBe('');
+ expect(f(['prefix', 'suffix'])).toBe('');
+ });
+ });
+
+ describe('ipToPaddedString', () => {
+ const f = StringUtils.ipToPaddedString;
+
+ it('it should return the formatted IP', () => {
+ expect(f('10.244.253.4')).toBe('010.244.253.004');
+ expect(f('0.24.3.4')).toBe('000.024.003.004');
});
});
});
diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js
index 63cc2d29e9..ee09f6cae2 100644
--- a/client/app/scripts/utils/string-utils.js
+++ b/client/app/scripts/utils/string-utils.js
@@ -22,6 +22,11 @@ function renderSvg(text, unit) {
}
+function padToThreeDigits(n) {
+ return `000${n}`.slice(-3);
+}
+
+
function makeFormatters(renderFn) {
const formatters = {
filesize(value) {
@@ -75,6 +80,11 @@ export function longestCommonPrefix(strArr) {
return (new LCP(strArr)).lcp();
}
+// Converts IPs from '10.244.253.4' to '010.244.253.004' format.
+export function ipToPaddedString(value) {
+ return value.match(/\d+/g).map(padToThreeDigits).join('.');
+}
+
// Formats metadata values. Add a key to the `formatters` obj
// that matches the `dataType` of the field. You must return an Object
// with the keys `value` and `title` defined.
diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go
index abafe79225..b1bb218762 100644
--- a/probe/kubernetes/reporter.go
+++ b/probe/kubernetes/reporter.go
@@ -28,7 +28,7 @@ const (
var (
PodMetadataTemplates = report.MetadataTemplates{
State: {ID: State, Label: "State", From: report.FromLatest, Priority: 2},
- IP: {ID: IP, Label: "IP", From: report.FromLatest, Priority: 3},
+ IP: {ID: IP, Label: "IP", From: report.FromLatest, Datatype: "ip", Priority: 3},
report.Container: {ID: report.Container, Label: "# Containers", From: report.FromCounters, Datatype: "number", Priority: 4},
Namespace: {ID: Namespace, Label: "Namespace", From: report.FromLatest, Priority: 5},
Created: {ID: Created, Label: "Created", From: report.FromLatest, Datatype: "datetime", Priority: 6},
@@ -39,8 +39,8 @@ var (
ServiceMetadataTemplates = report.MetadataTemplates{
Namespace: {ID: Namespace, Label: "Namespace", From: report.FromLatest, Priority: 2},
Created: {ID: Created, Label: "Created", From: report.FromLatest, Datatype: "datetime", Priority: 3},
- PublicIP: {ID: PublicIP, Label: "Public IP", From: report.FromLatest, Priority: 4},
- IP: {ID: IP, Label: "Internal IP", From: report.FromLatest, Priority: 5},
+ PublicIP: {ID: PublicIP, Label: "Public IP", From: report.FromLatest, Datatype: "ip", Priority: 4},
+ IP: {ID: IP, Label: "Internal IP", From: report.FromLatest, Datatype: "ip", Priority: 5},
report.Pod: {ID: report.Pod, Label: "# Pods", From: report.FromCounters, Datatype: "number", Priority: 6},
}
diff --git a/render/detailed/node.go b/render/detailed/node.go
index 4891fac450..16b532e8f7 100644
--- a/render/detailed/node.go
+++ b/render/detailed/node.go
@@ -148,7 +148,7 @@ var (
Label: "Services",
Columns: []Column{
{ID: report.Pod, Label: "# Pods", Datatype: "number"},
- {ID: kubernetes.IP, Label: "IP"},
+ {ID: kubernetes.IP, Label: "IP", Datatype: "ip"},
},
},
},
@@ -172,7 +172,7 @@ var (
Columns: []Column{
{ID: kubernetes.State, Label: "State"},
{ID: report.Container, Label: "# Containers", Datatype: "number"},
- {ID: kubernetes.IP, Label: "IP"},
+ {ID: kubernetes.IP, Label: "IP", Datatype: "ip"},
},
},
},
diff --git a/render/detailed/node_test.go b/render/detailed/node_test.go
index 1402e73dee..a4a3d74986 100644
--- a/render/detailed/node_test.go
+++ b/render/detailed/node_test.go
@@ -106,7 +106,7 @@ func TestMakeDetailedHostNode(t *testing.T) {
Columns: []detailed.Column{
{ID: kubernetes.State, Label: "State"},
{ID: report.Container, Label: "# Containers", Datatype: "number"},
- {ID: kubernetes.IP, Label: "IP"},
+ {ID: kubernetes.IP, Label: "IP", Datatype: "ip"},
},
Nodes: []detailed.NodeSummary{podNodeSummary},
},