Adding sorting to all collections. Some styling work

This commit is contained in:
Eric Herbrandson
2019-03-23 20:44:13 -05:00
parent 8105618b0e
commit 63fb853aac
54 changed files with 1250 additions and 696 deletions
+199
View File
@@ -896,6 +896,62 @@
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
"integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw=="
},
"@emotion/babel-utils": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/@emotion/babel-utils/-/babel-utils-0.6.10.tgz",
"integrity": "sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow==",
"requires": {
"@emotion/hash": "^0.6.6",
"@emotion/memoize": "^0.6.6",
"@emotion/serialize": "^0.9.1",
"convert-source-map": "^1.5.1",
"find-root": "^1.1.0",
"source-map": "^0.7.2"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
}
}
},
"@emotion/hash": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz",
"integrity": "sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ=="
},
"@emotion/memoize": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz",
"integrity": "sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ=="
},
"@emotion/serialize": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.9.1.tgz",
"integrity": "sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ==",
"requires": {
"@emotion/hash": "^0.6.6",
"@emotion/memoize": "^0.6.6",
"@emotion/unitless": "^0.6.7",
"@emotion/utils": "^0.8.2"
}
},
"@emotion/stylis": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.7.1.tgz",
"integrity": "sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ=="
},
"@emotion/unitless": {
"version": "0.6.7",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz",
"integrity": "sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg=="
},
"@emotion/utils": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.2.tgz",
"integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw=="
},
"@mrmlnc/readdir-enhanced": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",
@@ -2039,6 +2095,32 @@
"object.assign": "^4.1.0"
}
},
"babel-plugin-emotion": {
"version": "9.2.11",
"resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz",
"integrity": "sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@emotion/babel-utils": "^0.6.4",
"@emotion/hash": "^0.6.2",
"@emotion/memoize": "^0.6.1",
"@emotion/stylis": "^0.7.0",
"babel-plugin-macros": "^2.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"convert-source-map": "^1.5.0",
"find-root": "^1.1.0",
"mkdirp": "^0.5.1",
"source-map": "^0.5.7",
"touch": "^2.0.1"
},
"dependencies": {
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
}
}
},
"babel-plugin-istanbul": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz",
@@ -2079,6 +2161,11 @@
"resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.1.tgz",
"integrity": "sha512-vzZlo+yEB5YHqI6CRRTDojeT43J3Wf3C/MVkZW5UlbSeIIVUYRKtxaFT2L/VTv9mbIyatCW39+9g/SZolvwRUQ=="
},
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
},
"babel-plugin-syntax-object-rest-spread": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
@@ -3509,6 +3596,11 @@
}
}
},
"classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
},
"clean-css": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
@@ -3857,6 +3949,20 @@
"elliptic": "^6.0.0"
}
},
"create-emotion": {
"version": "9.2.12",
"resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.12.tgz",
"integrity": "sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA==",
"requires": {
"@emotion/hash": "^0.6.2",
"@emotion/memoize": "^0.6.1",
"@emotion/stylis": "^0.7.0",
"@emotion/unitless": "^0.6.2",
"csstype": "^2.5.2",
"stylis": "^3.5.0",
"stylis-rule-sheet": "^0.0.10"
}
},
"create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
@@ -4543,6 +4649,11 @@
"cssom": "0.3.x"
}
},
"csstype": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.3.tgz",
"integrity": "sha512-rINUZXOkcBmoHWEyu7JdHu5JMzkGRoMX4ov9830WNgxf5UYxcBUO0QTKAqeJ5EZfSdlrcJYkC8WwfVW7JYi4yg=="
},
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@@ -4911,6 +5022,14 @@
"utila": "~0.4"
}
},
"dom-helpers": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
"requires": {
"@babel/runtime": "^7.1.2"
}
},
"dom-serializer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
@@ -5032,6 +5151,15 @@
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k="
},
"emotion": {
"version": "9.2.12",
"resolved": "https://registry.npmjs.org/emotion/-/emotion-9.2.12.tgz",
"integrity": "sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ==",
"requires": {
"babel-plugin-emotion": "^9.2.11",
"create-emotion": "^9.2.12"
}
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -6240,6 +6368,11 @@
"pkg-dir": "^3.0.0"
}
},
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"find-up": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
@@ -10302,6 +10435,11 @@
"mimic-fn": "^1.0.0"
}
},
"memoize-one": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.0.0.tgz",
"integrity": "sha512-7g0+ejkOaI9w5x6LvQwmj68kUj6rxROywPSCqmclG/HBacmFnZqhVscQ8kovkn9FBCNJmOz6SY42+jnvZzDWdw=="
},
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@@ -15886,6 +16024,14 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.4.tgz",
"integrity": "sha512-fp+U98OMZcnduQ+NSEiQa4s/XMsbp+5KlydmkbESOw4P69iWZ68ZMFM5a2BuE0FgqPBKApJyRuYHR95jM8lAmg=="
},
"react-input-autosize": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz",
"integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==",
"requires": {
"prop-types": "^15.5.8"
}
},
"react-is": {
"version": "16.8.4",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.4.tgz",
@@ -16074,6 +16220,20 @@
}
}
},
"react-select": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-2.4.2.tgz",
"integrity": "sha512-5xFOQ6JJktkY5NTaHrc6x9mKwIjhNIiBkGic1j71uyY+ulFpRFra6f4WKLd9fuCylk4WjLpO5zDhdF4CAcwFzA==",
"requires": {
"classnames": "^2.2.5",
"emotion": "^9.1.2",
"memoize-one": "^5.0.0",
"prop-types": "^15.6.0",
"raf": "^3.4.0",
"react-input-autosize": "^2.2.1",
"react-transition-group": "^2.2.1"
}
},
"react-switch": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/react-switch/-/react-switch-4.1.0.tgz",
@@ -16082,6 +16242,17 @@
"prop-types": "^15.6.2"
}
},
"react-transition-group": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.7.0.tgz",
"integrity": "sha512-CzF22K0x6arjQO4AxkasMaiYcFG/QH0MhPNs45FmNsfWsQmsO9jv52sIZJAalnlryD5RgrrbLtV5CMJSokrrMA==",
"requires": {
"dom-helpers": "^3.3.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2",
"react-lifecycles-compat": "^3.0.4"
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
@@ -17968,6 +18139,16 @@
}
}
},
"stylis": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz",
"integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q=="
},
"stylis-rule-sheet": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
},
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
@@ -18278,6 +18459,24 @@
"hoek": "4.x.x"
}
},
"touch": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/touch/-/touch-2.0.2.tgz",
"integrity": "sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A==",
"requires": {
"nopt": "~1.0.10"
},
"dependencies": {
"nopt": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
"integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
"requires": {
"abbrev": "1"
}
}
}
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+1
View File
@@ -13,6 +13,7 @@
"react-dom": "^16.8.2",
"react-modal": "^3.8.1",
"react-scripts": "2.1.5",
"react-select": "^2.4.2",
"react-switch": "^4.1.0",
"swagger-parser": "^6.0.5",
"uuid": "^3.3.2",
+4 -7
View File
@@ -1,13 +1,10 @@
import React from 'react';
const LoadingSvg = props => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" {...props}>
<circle cx="50" cy="50" fill="none" strokeLinecap="round" r="40" strokeWidth="10" stroke="#326ce5" strokeDasharray="62.83185307 62.83185307" transform="rotate(28.717 50 50)">
<animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 50 50;360 50 50" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"/>
</circle>
<circle cx="50" cy="50" fill="none" strokeLinecap="round" r="30" strokeWidth="10" stroke="#555" strokeDasharray="54.97787144 54.97787144" strokeDashoffset="54.97787144" transform="rotate(-28.717 50 50)">
<animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 50 50;-360 50 50" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"/>
</circle>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 740" {...props}>
<path d="M381 118c-8 0-15 8-15 17v4l2 14 2 26-5 8v6a190 190 0 0 0-122 59l-6-4c-2 0-5 1-8-1-7-4-13-10-20-18l-10-10-3-2c-3-3-7-4-10-4-5 0-9 1-12 5-5 6-3 16 4 21v1l3 2 12 7 22 15 3 9 5 4c-25 38-37 84-30 132l-6 2c-2 2-4 5-7 6l-26 4-14 1-4 1c-9 3-14 10-13 18 2 8 11 13 19 11h1l4-1 13-5 25-8c4 0 7 2 9 3l6-1c15 46 45 83 84 106l-2 6c1 3 2 6 1 8-3 8-8 15-13 24-3 4-6 7-8 12l-2 4c-4 8-1 17 6 21 8 3 17-1 20-9h1l1-4 5-13c3-10 6-20 11-26 1-2 4-3 6-3l3-6a189 189 0 0 0 135 0l4 6c2 1 5 1 7 4 4 7 7 15 10 25l4 13 2 4c4 8 13 12 20 8 8-3 10-12 7-21l-2-3-8-12-13-23c-1-4 0-6 1-9l-2-6c40-24 70-62 84-106l6 1c2-1 4-3 8-3 8 1 16 4 26 8l13 5 4 1c9 2 17-3 19-11s-4-16-12-18l-5-1-14-1-26-4c-3-1-5-5-6-7l-6-1a189 189 0 0 0-30-132l5-5c0-2 0-5 2-8l22-15 12-7 4-3c7-5 8-15 3-21-5-7-15-7-22-2l-3 3-10 10-19 18c-3 2-7 1-9 1l-6 4c-31-33-75-55-121-59v-6c-2-2-5-4-5-8-1-8 0-16 1-26l2-14v-4c0-9-6-17-14-17zm-19 114l-4 77a13 13 0 0 1-21 10l-63-45a151 151 0 0 1 88-42zm37 0c33 4 64 19 88 42l-63 44a13 13 0 0 1-20-9h-1zm-148 71l58 51a13 13 0 0 1-5 23l-74 21c-4-34 4-67 21-95zm259 0a153 153 0 0 1 22 95l-74-21v-1a13 13 0 0 1-5-22l57-51zm-141 55h24l14 18-5 23-21 10-21-10-6-23zm75 63h4l76 13c-11 31-33 58-61 76l-30-71a13 13 0 0 1 11-18zm-127 0a13 13 0 0 1 11 18l-29 71c-27-18-49-44-61-76l76-13h3zm63 31l6 1c3 2 5 3 6 6l38 67a154 154 0 0 1-98 0l37-67c3-4 7-7 11-7z">
<animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 380 370;-360 380 370" keyTimes="0;1" dur="2s" begin="0s" repeatCount="indefinite"/>
</path>
</svg>
);
+16 -1
View File
@@ -1,6 +1,7 @@
import _ from 'lodash';
import React from 'react';
import Chart from './chart';
import {getPodCpuTotals} from '../utils/metricsHelpers';
import {parseCpu, TO_ONE_CPU} from '../utils/unitHelpers';
export default function CpuTotalsChart({items, metrics}) {
const totals = getPodCpuTotals(items, metrics);
@@ -17,3 +18,17 @@ export default function CpuTotalsChart({items, metrics}) {
</div>
);
}
function getPodCpuTotals(items, metrics) {
if (!items || !metrics) return null;
const metricsContainers = Object.values(metrics).flatMap(x => x.containers);
const podContainers = items
.flatMap(x => x.spec.containers)
.filter(x => x.resources && x.resources.requests);
const used = _.sumBy(metricsContainers, x => parseCpu(x.usage.cpu)) / TO_ONE_CPU;
const available = _.sumBy(podContainers, x => parseCpu(x.resources.requests.cpu)) / TO_ONE_CPU;
return {used, available};
}
+29 -12
View File
@@ -1,27 +1,42 @@
import React from 'react';
import moment from 'moment';
import {NoResults, hasResults} from './listViewHelpers';
import {TableBody} from './listViewHelpers';
import log from '../utils/log';
import ResourceSvg from '../art/resourceSvg';
import Sorter, {sortByDate} from './sorter';
export default function EventsPanel({items, filter, shortList}) {
export default function EventsPanel({items, filter, shortList, sort}) {
return (
<div className='contentPanel'>
<table>
<thead>
<tr>
<th className='th_icon'></th>
<th>Time</th>
<th className='th_icon'>
<Sorter field='involvedObject.kind' sort={sort}>Type</Sorter>
</th>
<th>
<Sorter field={sortByDate} sort={sort}>Time</Sorter>
</th>
{!shortList && (
<th>Name</th>
<th>
<Sorter field={sortByName} sort={sort}>Name</Sorter>
</th>
)}
<th>Reason</th>
<th>Message</th>
<th>
<Sorter field='reason' sort={sort}>Reason</Sorter>
</th>
<th>
<Sorter field='message' sort={sort}>Message</Sorter>
</th>
</tr>
</thead>
<tbody>
{hasResults(items) ? items.map(x => (
<TableBody
items={items}
filter={filter}
colSpan={shortList ? 4 : 5}
sort={sort}
row={x => (
<tr key={x.metadata.name}>
<td className='td_icon'>
<ResourceSvg
@@ -37,15 +52,17 @@ export default function EventsPanel({items, filter, shortList}) {
<td>{x.reason}</td>
<td>{x.message}</td>
</tr>
)) : (
<NoResults items={items} filter={filter} colSpan={shortList ? 4 : 5} />
)}
</tbody>
/>
</table>
</div>
);
}
function sortByName({involvedObject}) {
return `${involvedObject.namespace}:${involvedObject.name}`;
}
function getTypeClass(type) {
switch (type) {
case 'Normal':
+32 -20
View File
@@ -1,25 +1,49 @@
import _ from 'lodash';
import React from 'react';
import moment from 'moment';
import Loading from './loading';
import Sorter from './sorter';
import Sorter, {sortByDate} from './sorter';
import ResourceSvg from '../art/resourceSvg';
export function MetadataHeaders({includeNamespace, sortBy, sortDirection, onSort}) {
export function objectMap(items = {}) {
return Object.entries(items).map(([key, value]) => (
<div key={key}>
<span>{key}</span> <span title={value}>{value.length <= 50 ? value : `${value.substr(0, 50)}...`}</span>
</div>
));
}
export function TableBody({items, filter, colSpan, sort, row}) {
if (items && sort) {
const {field, direction} = sort;
items = _.orderBy(items, [field], [direction]); // eslint-disable-line no-param-reassign
}
return (
<tbody>
{items && items.length > 0 ? items.map(row) : (
<NoResults items={items} filter={filter} colSpan={colSpan} />
)}
</tbody>
);
}
export function MetadataHeaders({includeNamespace, sort}) {
return (
<>
<th className='th_icon'>
<Sorter text='Type' field='kind' sortBy={sortBy} sortDirection={sortDirection} onSort={onSort} />
<Sorter field='kind' sort={sort}>Type</Sorter>
</th>
<th>
<Sorter text='Name' field='metadata.name' sortBy={sortBy} sortDirection={sortDirection} onSort={onSort} />
<Sorter field='metadata.name' sort={sort}>Name</Sorter>
</th>
{includeNamespace && (
<th>
<Sorter text='Namespace' field='metadata.namespace' sortBy={sortBy} sortDirection={sortDirection} onSort={onSort} />
<Sorter field='metadata.namespace' sort={sort}>Namespace</Sorter>
</th>
)}
<th>
<Sorter text='Age' field='metadata.creationTimestamp' sortBy={sortBy} sortDirection={sortDirection} onSort={onSort} />
<Sorter field={sortByDate} sort={sort}>Age</Sorter>
</th>
</>
);
@@ -36,7 +60,7 @@ export function MetadataColumns({item, href, includeNamespace, isError}) {
<div className='td_iconLabel'>{item.kind}</div>
</td>
<td>
<a href={href}>{item.metadata.name}</a>
{href ? (<a href={href}>{item.metadata.name}</a>) : item.metadata.name}
</td>
{includeNamespace && (
@@ -52,11 +76,7 @@ export function MetadataColumns({item, href, includeNamespace, isError}) {
);
}
export function hasResults(items) {
return !!items && items.length > 0;
}
export function NoResults({items, filter, colSpan}) {
function NoResults({items, filter, colSpan}) {
if (!items) {
return (
<tr>
@@ -81,11 +101,3 @@ export function NoResults({items, filter, colSpan}) {
);
}
}
export function objectMap(items = {}) {
return Object.entries(items).map(([key, value]) => (
<div key={key}>
<span>{key}</span> <span title={value}>{value.length <= 50 ? value : `${value.substr(0, 50)}...`}</span>
</div>
));
}
+1 -1
View File
@@ -4,7 +4,7 @@ import './loading.scss';
const Loading = ({text}) => (
<div className='loading'>
<LoadingSvg />
<LoadingSvg className='loading_svg' />
<span className='loading_text'>{text != null ? text : 'Loading...'}</span>
</div>
);
+4 -4
View File
@@ -6,7 +6,7 @@
justify-content: center;
}
.loading_text {
font-size: $font-size-medium;
margin-left: 3px;
}
svg.loading_svg {
width: 30px;
height: 30px;
}
+16 -13
View File
@@ -1,4 +1,5 @@
import React from 'react';
import Select from 'react-select';
import Base from './base';
import api from '../services/api';
@@ -22,21 +23,23 @@ export default class NamespaceFilter extends Base {
}
render() {
const {namespace, namespaces = []} = this.state;
const {namespace = '', namespaces = []} = this.state;
const options = namespaces.map(x => ({value: x.metadata.name, label: x.metadata.name}));
options.unshift({value: '', label: 'All Namespaces'});
const value = options.find(x => x.value === namespace);
return (
<select
value={namespace}
className='select_namespace'
onChange={x => this.setNamespace(x.target.value)}
>
<>
<option value=''>All Namespaces</option>
{namespaces.map(x => (
<option key={x.metadata.name}>{x.metadata.name}</option>
))}
</>
</select>
<div className='select_namespace'>
<Select
className="react-select"
classNamePrefix="react-select"
value={value}
onChange={x => this.setNamespace(x.value)}
options={options}
/>
</div>
);
}
}
+37 -28
View File
@@ -2,24 +2,29 @@ import './podsPanel.scss';
import React from 'react';
import _ from 'lodash';
import Base from './base';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from './listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from './listViewHelpers';
import Sorter from './sorter';
import {parseRam, unparseRam, parseCpu, unparseCpu} from '../utils/unitHelpers';
export default class PodsPanel extends Base {
render() {
const {items, filter, metrics, skipNamespace, skipNodeName} = this.props;
const colSpan = 9 + !skipNamespace + !skipNodeName;
const {items, metrics, sort, filter, skipNamespace, skipNodeName} = this.props;
const col = 8 + !skipNamespace + !skipNodeName;
return (
<div className='contentPanel'>
<table>
<thead>
<tr>
<MetadataHeaders includeNamespace={!skipNamespace} />
{!skipNodeName && <th>Node</th>}
<th>Status</th>
<th>Containers</th>
<th>Restarts</th>
<MetadataHeaders sort={sort} includeNamespace={!skipNamespace} />
{!skipNodeName && (
<th><Sorter field='spec.nodeName' sort={sort}>Node</Sorter></th>
)}
<th><Sorter field='status.phase' sort={sort}>Status</Sorter></th>
<th><Sorter field={getContainerCount} sort={sort}>Containers</Sorter></th>
<th><Sorter field={getRestartCount} sort={sort}>Restarts</Sorter></th>
{/* TODO: support sorting by metrics */}
<th>
Cpu
<div className='podsPanel_label'>
@@ -35,32 +40,36 @@ export default class PodsPanel extends Base {
</tr>
</thead>
<tbody>
{hasResults(items) ? items.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
isError={x.status.phase !== 'Running'}
includeNamespace={!skipNamespace}
href={`#/pod/${x.metadata.namespace}/${x.metadata.name}`}
/>
{!skipNodeName && <td>{x.spec.nodeName}</td>}
<td>{x.status.phase}</td>
<td>{(x.spec.containers || []).length}</td>
<td>{_.sumBy(x.status.containerStatuses, 'restartCount')}</td>
<td><Cpu item={x} metrics={metrics} /></td>
<td><Ram item={x} metrics={metrics} /></td>
</tr>
)) : (
<NoResults items={items} filter={filter} colSpan={colSpan} />
)}
</tbody>
<TableBody items={items} filter={filter} sort={sort} colSpan={col} row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
isError={x.status.phase !== 'Running'}
includeNamespace={!skipNamespace}
href={`#/pod/${x.metadata.namespace}/${x.metadata.name}`}
/>
{!skipNodeName && <td>{x.spec.nodeName}</td>}
<td>{x.status.phase}</td>
<td>{getContainerCount(x)}</td>
<td>{getRestartCount(x)}</td>
<td><Cpu item={x} metrics={metrics} /></td>
<td><Ram item={x} metrics={metrics} /></td>
</tr>
)} />
</table>
</div>
);
}
}
function getContainerCount({spec}) {
return spec.containers ? spec.containers.length : 0;
}
function getRestartCount({status}) {
return _.sumBy(status.containerStatuses, 'restartCount');
}
function Cpu({item, metrics}) {
if (!item || !metrics) return null;
+16 -1
View File
@@ -1,6 +1,7 @@
import _ from 'lodash';
import React from 'react';
import Chart from './chart';
import {getPodRamTotals} from '../utils/metricsHelpers';
import {parseRam, TO_GB} from '../utils/unitHelpers';
export default function RamChart({items, metrics}) {
const totals = getPodRamTotals(items, metrics);
@@ -12,3 +13,17 @@ export default function RamChart({items, metrics}) {
</div>
);
}
export function getPodRamTotals(items, metrics) {
if (!items || !metrics) return null;
const metricsContainers = Object.values(metrics).flatMap(x => x.containers);
const podContainers = items
.flatMap(x => x.spec.containers)
.filter(x => x.resources && x.resources.requests);
const used = _.sumBy(metricsContainers, x => parseRam(x.usage.memory)) / TO_GB;
const available = _.sumBy(podContainers, x => parseRam(x.resources.requests.memory)) / TO_GB;
return {used, available};
}
+19 -16
View File
@@ -2,12 +2,15 @@ import './replicaSetsPanel.scss';
import React from 'react';
import Switch from 'react-switch';
import Base from './base';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from './listViewHelpers';
import Sorter from './sorter';
import {MetadataHeaders, MetadataColumns, TableBody} from './listViewHelpers';
export default class ReplicaSetsPanel extends Base {
state = {activeOnly: true};
render() {
const {items, filter, includeNamespace = true} = this.props;
const {activeOnly = true} = this.state || {};
const {items, sort, filter, includeNamespace = true} = this.props;
const {activeOnly} = this.state;
const filtered = filterItems(activeOnly, items);
return (
@@ -15,11 +18,13 @@ export default class ReplicaSetsPanel extends Base {
<table>
<thead>
<tr>
<MetadataHeaders includeNamespace={includeNamespace} />
<th>Generations</th>
<MetadataHeaders sort={sort} includeNamespace={includeNamespace} />
<th>
<Sorter field='status.observedGeneration' sort={sort}>Generations</Sorter>
</th>
<th className='replicaSetsPanel_replicas'>
Replicas
<label>
<Sorter field='spec.replicas' sort={sort}>Replicas</Sorter>
<label className='replicaSetsPanel_switch'>
<Switch
checked={activeOnly}
onChange={x => this.setState({activeOnly: x})}
@@ -34,8 +39,12 @@ export default class ReplicaSetsPanel extends Base {
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<TableBody
items={filtered}
filter={filter}
sort={sort}
colSpan={5 + !!includeNamespace}
row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
@@ -45,14 +54,8 @@ export default class ReplicaSetsPanel extends Base {
<td>{x.status.observedGeneration}</td>
<td>{x.spec.replicas} / {x.status.replicas}</td>
</tr>
)) : (
<NoResults
colSpan={5 + includeNamespace}
items={filtered}
filter={filter}
/>
)}
</tbody>
/>
</table>
</div>
);
@@ -9,3 +9,9 @@
.replicaSetsPanel_label {
font-size: $font-size-small;
}
.replicaSetsPanel_switch {
display: flex;
flex-direction: column;
align-items: center;
}
+43 -17
View File
@@ -4,21 +4,33 @@ import Base from './base';
import ArrowUpSvg from '../art/arrowUpSvg';
import ArrowDownSvg from '../art/arrowDownSvg';
export function defaultSortInfo(target, field = 'metadata.name', direction = 'asc') {
return {
field,
direction,
onSort: typeof target === 'function' ? target : defaultOnSort,
};
function defaultOnSort(sort) {
target.setState({sort});
}
}
export function sortByDate(x) {
return -Date.parse(x.metadata.creationTimestamp);
}
export default class Sorter extends Base {
render() {
const {text, field, sortBy, sortDirection, onSort} = this.props;
const isSelected = (sortBy && field === sortBy);
const isUpSelected = (isSelected && sortDirection === 'asc');
const isDownSelected = (isSelected && sortDirection === 'desc');
const {children, sort} = this.props;
return (
<div className={onSort ? 'sorter' : undefined} onClick={() => this.sort()}>
<span>{text}</span>
{onSort && (
<span className={`sorter_arrows ${isSelected && 'sorter_active'}`}>
<ArrowUpSvg className={isUpSelected ? 'sorter_active' : undefined} />
<ArrowDownSvg className={isDownSelected ? 'sorter_active' : undefined} />
<div className={sort ? 'sorter' : undefined} onClick={() => sort && this.sort()}>
<span>{children}</span>
{sort && (
<span className={`sorter_arrows ${this.isSelected() && 'sorter_active'}`}>
<ArrowUpSvg className={this.isUpSelected() ? 'sorter_active' : undefined} />
<ArrowDownSvg className={this.isDownSelected() ? 'sorter_active' : undefined} />
</span>
)}
</div>
@@ -26,13 +38,27 @@ export default class Sorter extends Base {
}
sort() {
const {field, sortBy, sortDirection, onSort} = this.props;
if (!onSort) return;
const {field, sort} = this.props;
if (!sort) return;
const isSelected = (sortBy && field === sortBy);
const isDownSelected = (isSelected && sortDirection === 'desc');
const direction = !isSelected || isDownSelected ? 'asc' : 'desc';
const direction = !this.isSelected() || this.isDownSelected() ? 'asc' : 'desc';
sort.field = field;
sort.direction = direction;
sort.onSort(sort);
}
onSort(field, direction);
isSelected() {
const {sort, field} = this.props;
return (sort && field === sort.field);
}
isUpSelected() {
const {sort} = this.props;
return (this.isSelected() && sort.direction === 'asc');
}
isDownSelected() {
const {sort} = this.props;
return (this.isSelected() && sort.direction === 'desc');
}
}
+3 -2
View File
@@ -7,6 +7,7 @@
}
.sorter_arrows {
margin-top: -2px;
margin-left: 3px;
display: flex;
flex-direction: column;
@@ -17,8 +18,8 @@
svg {
fill: $color-light;
width: 8px;
height: 8px;
width: 6px;
height: 6px;
}
}
+36 -12
View File
@@ -1,18 +1,42 @@
select.select_namespace {
border: none;
.select_namespace {
margin-left: 15px;
margin-top: 10px;
display: inline-block;
width: 155px;
}
-webkit-appearance: none;
-moz-appearance: none;
div.react-select__control {
border: 1px solid $color-lightest;
border-radius: 0;
min-height: initial;
}
div.react-select__control:hover {
border-color: $color-light;
box-shadow: none;
}
div.react-select__indicator {
padding: 0;
}
div.react-select__menu {
border-radius: 0;
}
div.react-select__option {
background-color: transparent;
background-position: right 50%;
background-repeat: no-repeat;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAMCAYAAABSgIzaAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDZFNDEwNjlGNzFEMTFFMkJEQ0VDRTM1N0RCMzMyMkIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDZFNDEwNkFGNzFEMTFFMkJEQ0VDRTM1N0RCMzMyMkIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0NkU0MTA2N0Y3MUQxMUUyQkRDRUNFMzU3REIzMzIyQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0NkU0MTA2OEY3MUQxMUUyQkRDRUNFMzU3REIzMzIyQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuGsgwQAAAA5SURBVHjaYvz//z8DOYCJgUxAf42MQIzTk0D/M+KzkRGPoQSdykiKJrBGpOhgJFYTWNEIiEeAAAMAzNENEOH+do8AAAAASUVORK5CYII=);
padding-right: 1.5em;
}
&:focus {
outline: none !important;
}
div.react-select__option:hover {
background-color: $color-lightest;
}
div.react-select__option--is-selected {
color: $color-accent;
background-color: $color-lightest;
}
span.react-select__indicator-separator {
display: none;
}
+5 -1
View File
@@ -103,7 +103,11 @@ function metrics(url, cb) {
}
}
return () => clearInterval(handel);
return cancel;
function cancel() {
clearInterval(handel);
}
}
function apiFactory(apiType, kind) {
+105 -66
View File
@@ -55,6 +55,111 @@ export async function request(path, params, autoLogoutOnAuthError = true) {
return response.json();
}
export async function streamResult(url, name, cb) {
let isCancelled = false;
let socket;
run();
return cancel;
async function run() {
try {
const item = await request(`${url}/${name}`);
if (isCancelled) return;
cb(item);
const fieldSelector = encodeURIComponent(`metadata.name=${name}`);
const watchUrl = `${url}?watch=1&fieldSelector=${fieldSelector}`;
socket = stream(watchUrl, x => cb(x.object));
} catch (err) {
log.error('Error in api request', {err, url});
}
}
function cancel() {
if (isCancelled) return;
isCancelled = true;
if (socket) socket.cancel();
}
}
export async function streamResults(url, cb) {
const results = {};
let isCancelled = false;
let socket;
run();
return cancel;
async function run() {
try {
const {kind, items, metadata} = await request(url);
if (isCancelled) return;
add(items, kind);
const watchUrl = `${url}?watch=1&resourceVersion=${metadata.resourceVersion}`;
socket = stream(watchUrl, update);
} catch (err) {
log.error('Error in api request', {err, url});
}
}
function cancel() {
if (isCancelled) return;
isCancelled = true;
if (socket) socket.cancel();
}
function add(items, kind) {
const fixedKind = kind.slice(0, -4); // Trim off the word "List" from the end of the string
for (const item of items) {
item.kind = fixedKind;
results[item.metadata.uid] = item;
}
push();
}
function update({type, object}) {
object.actionType = type; // eslint-disable-line no-param-reassign
switch (type) {
case 'ADDED':
results[object.metadata.uid] = object;
break;
case 'MODIFIED': {
const existing = results[object.metadata.uid];
const currentVersion = parseInt(existing.metadata.resourceVersion, 10);
const newVersion = parseInt(object.metadata.resourceVersion, 10);
if (currentVersion < newVersion) {
Object.assign(existing, object);
}
break;
}
case 'DELETED':
delete results[object.metadata.uid];
break;
case 'ERROR':
log.error('Error in update', {type, object});
break;
default:
log.error('Unknown update type', type);
}
push();
}
function push() {
const values = Object.values(results);
cb(values);
}
}
export function stream(url, cb, isJson = true, additionalProtocols) {
let connection;
let isCancelled;
@@ -134,72 +239,6 @@ function connectStream(path, cb, onFail, isJson, additionalProtocols = []) {
}
}
export async function streamResult(url, name, cb) {
const item = await request(`${url}/${name}`);
cb(item);
const fieldSelector = encodeURIComponent(`metadata.name=${name}`);
const watchUrl = `${url}?watch=1&fieldSelector=${fieldSelector}`;
const {cancel} = stream(watchUrl, x => cb(x.object));
return cancel;
}
export async function streamResults(url, cb) {
const results = {};
const {kind, items, metadata} = await request(url);
const fixedKind = kind.slice(0, -4); // Trim off the word "List" from the end of the string
add();
const watchUrl = `${url}?watch=1&resourceVersion=${metadata.resourceVersion}`;
const {cancel} = stream(watchUrl, update);
return cancel;
function add() {
for (const item of items) {
item.kind = fixedKind;
results[item.metadata.uid] = item;
}
push(results, cb);
}
function update({type, object}) {
object.actionType = type; // eslint-disable-line no-param-reassign
switch (type) {
case 'ADDED':
results[object.metadata.uid] = object;
break;
case 'MODIFIED': {
const existing = results[object.metadata.uid];
const currentVersion = parseInt(existing.metadata.resourceVersion, 10);
const newVersion = parseInt(object.metadata.resourceVersion, 10);
if (currentVersion < newVersion) {
Object.assign(existing, object);
}
break;
}
case 'DELETED':
delete results[object.metadata.uid];
break;
case 'ERROR':
log.error('Error in update', {type, object});
break;
default:
log.error('Unknown update type', type);
}
push(results, cb);
}
}
function push(results, cb) {
const items = Object.values(results);
cb(items);
}
function combinePath(base, path) {
if (base.endsWith('/')) base = base.slice(0, -1); // eslint-disable-line no-param-reassign
if (path.startsWith('/')) path = path.slice(1); // eslint-disable-line no-param-reassign
+4 -2
View File
@@ -1,7 +1,9 @@
export default function test(filter = '', ...values) {
const lowerCaseFilter = filter.toLowerCase();
return values.some(x => (x || '').toLowerCase().includes(lowerCaseFilter));
const value = filter.toLowerCase();
return values
.filter(x => !!x)
.some(x => x.toLowerCase().includes(value));
}
export function filterByOwner(items, owner) {
+1 -63
View File
@@ -1,40 +1,6 @@
import _ from 'lodash';
import {parseCpu, parseRam} from './unitHelpers';
const TO_GB = 1024 * 1024 * 1024;
const TO_ONE_CPU = 1000000000;
export function getNodeMetrics(nodes, metrics) {
if (!nodes || !metrics) return null;
const names = _.map(nodes, x => x.metadata.name);
const filteredMetrics = metrics.filter(x => names.includes(x.metadata.name));
return _.keyBy(filteredMetrics, 'metadata.name');
}
export function getNodeCpuTotals(items, metrics) {
if (!items || !metrics) return null;
const metricValues = Object.values(metrics);
const used = _.sumBy(metricValues, x => parseCpu(x.usage.cpu)) / TO_ONE_CPU;
const available = _.sumBy(items, x => parseCpu(x.status.capacity.cpu)) / TO_ONE_CPU;
return {used, available};
}
export function getNodeRamTotals(items, metrics) {
if (!items || !metrics) return null;
const metricValues = Object.values(metrics);
const used = _.sumBy(metricValues, x => parseRam(x.usage.memory)) / TO_GB;
const available = _.sumBy(items, x => parseRam(x.status.capacity.memory)) / TO_GB;
return {used, available};
}
export function getPodMetrics(pods, metrics) {
export default function getPodMetrics(pods, metrics) {
if (!pods || !metrics) return null;
const names = _.map(pods, x => x.metadata.name);
@@ -42,31 +8,3 @@ export function getPodMetrics(pods, metrics) {
return _.keyBy(filteredMetrics, 'metadata.name');
}
export function getPodCpuTotals(items, metrics) {
if (!items || !metrics) return null;
const metricsContainers = Object.values(metrics).flatMap(x => x.containers);
const podContainers = items
.flatMap(x => x.spec.containers)
.filter(x => x.resources && x.resources.requests);
const used = _.sumBy(metricsContainers, x => parseCpu(x.usage.cpu)) / TO_ONE_CPU;
const available = _.sumBy(podContainers, x => parseCpu(x.resources.requests.cpu)) / TO_ONE_CPU;
return {used, available};
}
export function getPodRamTotals(items, metrics) {
if (!items || !metrics) return null;
const metricsContainers = Object.values(metrics).flatMap(x => x.containers);
const podContainers = items
.flatMap(x => x.spec.containers)
.filter(x => x.resources && x.resources.requests);
const used = _.sumBy(metricsContainers, x => parseRam(x.usage.memory)) / TO_GB;
const available = _.sumBy(podContainers, x => parseRam(x.resources.requests.memory)) / TO_GB;
return {used, available};
}
+17
View File
@@ -2,6 +2,23 @@ import _ from 'lodash';
const RAM_TYPES = ['Bi', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei'];
export const TO_GB = 1024 * 1024 * 1024;
export const TO_ONE_CPU = 1000000000;
export function parseDiskSpace(value) {
if (!value) return 0;
const number = parseInt(value, 10);
if (value.endsWith('Ki')) return number * 1024;
if (value.endsWith('Mi')) return number * 1024 * 1024;
if (value.endsWith('Gi')) return number * 1024 * 1024 * 1024;
if (value.endsWith('Ti')) return number * 1024 * 1024 * 1024 * 1024;
if (value.endsWith('Pi')) return number * 1024 * 1024 * 1024 * 1024 * 1024;
if (value.endsWith('Ei')) return number * 1024 * 1024 * 1024 * 1024 * 1024 * 1024;
return number;
}
export function parseRam(value) {
if (!value) return 0;
+14
View File
@@ -15,8 +15,22 @@ const Account = () => (
<div className='contentPanel'>
<h3>Current User</h3>
<pre>{getJson()}</pre>
</div>
<div className='contentPanel'>
<h3>Learn More</h3>
<div>Follow K8dash on <a href='https://github.com/herbrandson/k8dash'>GitHub</a></div>
<div>or at <a href='https://cloud.docker.com/u/herbrandson/repository/docker/herbrandson/k8dash'>DockerHub</a></div>
</div>
<div className='contentPanel'>
<h3>Special Thanks</h3>
<div>Icons made by <a href='https://www.flaticon.com/authors/dave-gandy' title='Dave Gandy'>Dave Gandy</a></div>
<div>from <a href='https://www.flaticon.com/' title='Flaticon'>www.flaticon.com</a></div>
<div>is licensed by <a href='http://creativecommons.org/licenses/by/3.0/' title='Creative Commons BY 3.0'>CC 3.0 BY</a></div>
</div>
</div>
);
+9 -13
View File
@@ -5,7 +5,7 @@ import api from '../services/api';
import ItemHeader from '../components/itemHeader';
import Loading from '../components/loading';
import MetadataFields from '../components/metadataFields';
import {NoResults, hasResults} from '../components/listViewHelpers';
import {TableBody} from '../components/listViewHelpers';
import SaveButton from '../components/saveButton';
import DeleteButton from '../components/deleteButton';
@@ -59,18 +59,14 @@ export default class ClusterRole extends Base {
</tr>
</thead>
<tbody>
{hasResults(rules) ? rules.map((x, i) => (
<tr key={i}>
<td>{_.map(x.resources, toDiv)}</td>
<td>{_.map(x.resourceNames, toDiv)}</td>
<td>{_.map(x.verbs, toDiv)}</td>
<td>{_.map(x.apiGroups, toDiv)}</td>
</tr>
)) : (
<NoResults items={rules} colSpan='4' />
)}
</tbody>
<TableBody items={rules} colSpan='4' row={(x, i) => (
<tr key={i}>
<td>{_.map(x.resources, toDiv)}</td>
<td>{_.map(x.resourceNames, toDiv)}</td>
<td>{_.map(x.verbs, toDiv)}</td>
<td>{_.map(x.apiGroups, toDiv)}</td>
</tr>
)} />
</table>
</div>
</div>
+32 -23
View File
@@ -5,14 +5,19 @@ import ItemHeader from '../components/itemHeader';
import Field from '../components/field';
import Loading from '../components/loading';
import MetadataFields from '../components/metadataFields';
import {NoResults, hasResults} from '../components/listViewHelpers';
import {TableBody} from '../components/listViewHelpers';
import SaveButton from '../components/saveButton';
import DeleteButton from '../components/deleteButton';
import ResourceSvg from '../art/resourceSvg';
import Sorter, {defaultSortInfo} from '../components/sorter';
const service = api.clusterRoleBinding;
export default class ClusterRoleBinding extends Base {
state = {
sort: defaultSortInfo(this, 'name'),
};
componentDidMount() {
const {name} = this.props;
@@ -23,7 +28,7 @@ export default class ClusterRoleBinding extends Base {
render() {
const {name} = this.props;
const {item} = this.state || {};
const {item, sort} = this.state;
const subjects = item && item.subjects;
return (
@@ -60,30 +65,34 @@ export default class ClusterRoleBinding extends Base {
<table>
<thead>
<tr>
<th className='th_icon'></th>
<th>Name</th>
<th>Namespace</th>
<th>Api Group</th>
<th className='th_icon'>
<Sorter sort={sort} field='kind'>Type</Sorter>
</th>
<th>
<Sorter sort={sort} field='name'>Name</Sorter>
</th>
<th>
<Sorter sort={sort} field='namespace'>Namespace</Sorter>
</th>
<th>
<Sorter sort={sort} field='apiGroup'>Api Group</Sorter>
</th>
</tr>
</thead>
<tbody>
{hasResults(subjects) ? subjects.map(x => (
<tr key={x.name}>
<td>
<ResourceSvg resource={x.kind} />
<div className='td_iconLabel'>{x.kind}</div>
</td>
<td>
{x.kind === 'ServiceAccount' ? (<a href={getSubjectHref(x)}>{x.name}</a>) : x.name}
</td>
<td>{x.namespace}</td>
<td>{x.apiGroup}</td>
</tr>
)) : (
<NoResults items={subjects} colSpan='4' />
)}
</tbody>
<TableBody items={subjects} colSpan='4' sort={sort} row={x => (
<tr key={x.name}>
<td>
<ResourceSvg resource={x.kind} />
<div className='td_iconLabel'>{x.kind}</div>
</td>
<td>
{x.kind === 'ServiceAccount' ? (<a href={getSubjectHref(x)}>{x.name}</a>) : x.name}
</td>
<td>{x.namespace}</td>
<td>{x.apiGroup}</td>
</tr>
)} />
</table>
</div>
</div>
+18 -18
View File
@@ -3,9 +3,15 @@ import Base from '../components/base';
import Filter from '../components/filter';
import api from '../services/api';
import test from '../utils/filterHelper';
import {MetadataColumns, MetadataHeaders, NoResults, hasResults} from '../components/listViewHelpers';
import {defaultSortInfo} from '../components/sorter';
import {MetadataColumns, MetadataHeaders, TableBody} from '../components/listViewHelpers';
export default class ConfigMaps extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
setNamespace(namespace) {
this.setState({items: null});
@@ -15,7 +21,7 @@ export default class ConfigMaps extends Base {
}
render() {
const {items, filter = ''} = this.state || {};
const {items, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
@@ -31,25 +37,19 @@ export default class ConfigMaps extends Base {
<table>
<thead>
<tr>
<MetadataHeaders
includeNamespace={true}
/>
<MetadataHeaders includeNamespace={true} sort={sort} />
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/configmap/${x.metadata.namespace}/${x.metadata.name}`}
/>
</tr>
)) : (
<NoResults colSpan='4' items={filtered} filter={filter} />
)}
</tbody>
<TableBody items={filtered} filter={filter} colSpan='4' sort={sort} row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/configmap/${x.metadata.namespace}/${x.metadata.name}`}
/>
</tr>
)} />
</table>
</div>
</div>
+21 -4
View File
@@ -12,12 +12,18 @@ import PodsPanel from '../components/podsPanel';
import RamChart from '../components/ramChart';
import SaveButton from '../components/saveButton';
import api from '../services/api';
import {getPodMetrics} from '../utils/metricsHelpers';
import getPodMetrics from '../utils/metricsHelpers';
import {filterByOwner} from '../utils/filterHelper';
import {defaultSortInfo} from '../components/sorter';
const service = api.cronJob;
export default class CronJob extends Base {
state = {
podsSort: defaultSortInfo(x => this.setState({podsSort: x})),
eventsSort: defaultSortInfo(x => this.setState({eventsSort: x})),
};
componentDidMount() {
const {namespace, name} = this.props;
@@ -31,7 +37,7 @@ export default class CronJob extends Base {
render() {
const {namespace, name} = this.props;
const {item, pods, events, metrics} = this.state || {};
const {item, pods, events, metrics, podsSort, eventsSort} = this.state;
const filteredPods = filterByOwner(pods, item);
const filteredEvents = filterByOwner(events, item);
@@ -75,8 +81,19 @@ export default class CronJob extends Base {
<ContainersPanel spec={item && item.spec.jobTemplate.spec.template.spec} />
{/* TODO: this actually need to be a list of jobs */}
<PodsPanel items={filteredPods} metrics={filteredMetrics} skipNamespace={true} />
<EventsPanel shortList={true} items={filteredEvents} />
<PodsPanel
items={filteredPods}
sort={podsSort}
metrics={filteredMetrics}
skipNamespace={true}
/>
<EventsPanel
sort={eventsSort}
shortList={true}
items={filteredEvents}
/>
</div>
);
}
+10 -4
View File
@@ -11,13 +11,19 @@ import PodsPanel from '../components/podsPanel';
import Chart from '../components/chart';
import RamChart from '../components/ramChart';
import CpuChart from '../components/cpuChart';
import {getPodMetrics} from '../utils/metricsHelpers';
import getPodMetrics from '../utils/metricsHelpers';
import {filterByOwner} from '../utils/filterHelper';
import ContainersPanel from '../components/containersPanel';
import {defaultSortInfo} from '../components/sorter';
const service = api.daemonSet;
export default class DaemonSet extends Base {
state = {
podsSort: defaultSortInfo(x => this.setState({podsSort: x})),
eventsSort: defaultSortInfo(x => this.setState({eventsSort: x})),
};
componentDidMount() {
const {namespace, name} = this.props;
@@ -31,7 +37,7 @@ export default class DaemonSet extends Base {
render() {
const {namespace, name} = this.props;
const {item, pods, events, metrics} = this.state || {};
const {item, pods, events, metrics, podsSort, eventsSort} = this.state;
const filteredPods = filterByOwner(pods, item);
const filteredEvents = filterByOwner(events, item);
@@ -74,8 +80,8 @@ export default class DaemonSet extends Base {
</div>
<ContainersPanel spec={item && item.spec.template.spec} />
<PodsPanel items={filteredPods} metrics={filteredMetrics} skipNamespace={true} />
<EventsPanel shortList={true} items={filteredEvents} />
<PodsPanel items={filteredPods} sort={podsSort} metrics={filteredMetrics} skipNamespace={true} />
<EventsPanel sort={eventsSort} shortList={true} items={filteredEvents} />
</div>
);
}
+11 -7
View File
@@ -1,12 +1,17 @@
import _ from 'lodash';
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import api from '../services/api';
import EventsPanel from '../components/eventsPanel';
import test from '../utils/filterHelper';
import {defaultSortInfo, sortByDate} from '../components/sorter';
export default class Dashboard extends Base {
state = {
filter: '',
sort: defaultSortInfo(this, sortByDate),
};
componentDidMount() {
this.registerApi({
events: api.event.list(null, events => this.setState({events})),
@@ -16,8 +21,7 @@ export default class Dashboard extends Base {
}
render() {
const {events, nodes, pods, filter = ''} = this.state || {};
const {events, nodes, pods, sort, filter} = this.state;
const filteredEvents = filterEvents(events, filter);
return (
@@ -39,7 +43,7 @@ export default class Dashboard extends Base {
</div>
</div>
<EventsPanel items={filteredEvents} filter={filter} />
<EventsPanel items={filteredEvents} filter={filter} sort={sort} />
</div>
);
}
@@ -48,7 +52,7 @@ export default class Dashboard extends Base {
function filterEvents(events, filter) {
if (!events) return null;
return _.sortBy(events, x => -Date.parse(x.metadata.creationTimestamp))
.slice(0, 1000)
.filter(x => test(filter, x.involvedObject.name, x.involvedObject.namespace, x.message));
return events
.filter(x => test(filter, x.involvedObject.name, x.involvedObject.namespace, x.message))
.slice(0, 1000);
}
+37 -5
View File
@@ -15,11 +15,18 @@ import SaveButton from '../components/saveButton';
import ScaleButton from '../components/scaleButton';
import api from '../services/api';
import {filterByOwner, filterByOwners} from '../utils/filterHelper';
import {getPodMetrics} from '../utils/metricsHelpers';
import getPodMetrics from '../utils/metricsHelpers';
import {defaultSortInfo} from '../components/sorter';
const service = api.deployment;
export default class Deployment extends Base {
state = {
replicaSetsSort: defaultSortInfo(x => this.setState({replicaSetsSort: x})),
podsSort: defaultSortInfo(x => this.setState({podsSort: x})),
eventsSort: defaultSortInfo(x => this.setState({eventsSort: x})),
};
componentDidMount() {
const {namespace, name} = this.props;
@@ -34,7 +41,16 @@ export default class Deployment extends Base {
render() {
const {namespace, name} = this.props;
const {item, events, replicaSets, pods, metrics} = this.state || {};
const {
item,
events,
replicaSets,
pods,
metrics,
replicaSetsSort,
podsSort,
eventsSort,
} = this.state;
const filteredEvents = filterByOwner(events, item);
const filteredReplicaSets = filterByOwner(replicaSets, item);
@@ -84,9 +100,25 @@ export default class Deployment extends Base {
</div>
<ContainersPanel spec={item && item.spec.template.spec} />
<ReplicaSetsPanel items={filteredReplicaSets} includeNamespace={false} />
<PodsPanel items={filteredPods} metrics={filteredMetrics} skipNamespace={true} />
<EventsPanel shortList={true} items={filteredEvents} />
<ReplicaSetsPanel
items={filteredReplicaSets}
sort={replicaSetsSort}
includeNamespace={false}
/>
<PodsPanel
items={filteredPods}
sort={podsSort}
metrics={filteredMetrics}
skipNamespace={true}
/>
<EventsPanel
shortList={true}
sort={eventsSort}
items={filteredEvents}
/>
</div>
);
}
+15 -11
View File
@@ -1,5 +1,6 @@
import 'xterm/dist/xterm.css';
import React from 'react';
import Select from 'react-select';
import {Terminal} from 'xterm';
import Base from '../components/base';
import api from '../services/api';
@@ -29,7 +30,7 @@ export default class Exec extends Base {
const {namespace, name} = this.props;
const exec = api.exec(namespace, name, container, items => this.onData(items));
this.socket = exec.getSocket();
this.socket = exec.getSocket(); // TODO: this won't work if the socket failes
this.registerApi({cancel: exec.cancel});
if (this.xterm) this.xterm.reset();
@@ -68,22 +69,25 @@ export default class Exec extends Base {
render() {
const {namespace, name} = this.props;
const {container, containers} = this.state || {};
const {container, containers = []} = this.state || {};
const options = containers.map(x => ({value: x, label: x}));
const selected = options.find(x => x.value === container);
return (
<div id='content'>
<div id='header'>
<span className='header_label'>{['Pod Logs', namespace, name].join(' • ')}</span>
<select
value={container}
className='select_namespace'
onChange={x => this.setContainer(x.target.value)}
>
{containers && containers.map(x => (
<option key={x}>{x}</option>
))}
</select>
<div className='select_namespace'>
<Select
className="react-select"
classNamePrefix="react-select"
value={selected}
options={options}
onChange={x => this.setContainer(x.value)}
/>
</div>
</div>
<div id='xterm' ref={x => this.onXTermRef(x)} className='contentPanel'>
+6 -10
View File
@@ -1,10 +1,10 @@
import React from 'react';
import React, {Fragment} from 'react';
import Base from '../components/base';
import api from '../services/api';
import ItemHeader from '../components/itemHeader';
import Loading from '../components/loading';
import MetadataFields from '../components/metadataFields';
import {NoResults, hasResults} from '../components/listViewHelpers';
import {TableBody} from '../components/listViewHelpers';
import SaveButton from '../components/saveButton';
import DeleteButton from '../components/deleteButton';
@@ -58,9 +58,8 @@ export default class Ingress extends Base {
</tr>
</thead>
<tbody>
{hasResults(rules) ? rules.map(rule => (
<>
<TableBody items={rules} colSpan='4' row={rule => (
<Fragment key={rule.host}>
{rule.http.paths.map(path => (
<tr key={`${rule.host}:${path.path}`}>
<td>{rule.host}</td>
@@ -69,11 +68,8 @@ export default class Ingress extends Base {
<td>{path.backend && path.backend.servicePort}</td>
</tr>
))}
</>
)) : (
<NoResults colSpan='4' items={rules} />
)}
</tbody>
</Fragment>
)} />
</table>
</div>
</div>
+32 -19
View File
@@ -2,11 +2,17 @@ import _ from 'lodash';
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import api from '../services/api';
import test from '../utils/filterHelper';
import {defaultSortInfo} from '../components/sorter';
export default class Ingresses extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
setNamespace(namespace) {
this.setState({items: null});
@@ -16,8 +22,8 @@ export default class Ingresses extends Base {
}
render() {
const {items, filter = ''} = this.state || {};
const filtered = items && items.filter(x => test(filter, x.metadata.name));
const {items, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name, getHosts(x, ' '), getPaths(x, ' ')));
return (
<div id='content'>
@@ -32,30 +38,37 @@ export default class Ingresses extends Base {
<table>
<thead>
<tr>
<MetadataHeaders includeNamespace={true} />
<MetadataHeaders includeNamespace={true} sort={sort} />
<th>Hosts</th>
<th>Paths</th>
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/ingress/${x.metadata.namespace}/${x.metadata.name}`}
/>
<td>{_.map(x.spec.rules, y => y.host).join(' • ')}</td>
<td>{_.flatMap(x.spec.rules, y => y.http.paths).map(y => y.path).join(' • ')}</td>
</tr>
)) : (
<NoResults items={filtered} filter={filter} colSpan='6' />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='6' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/ingress/${x.metadata.namespace}/${x.metadata.name}`}
/>
<td>{getHosts(x, ' • ')}</td>
<td>{getPaths(x, ' • ')}</td>
</tr>
)} />
</table>
</div>
</div>
);
}
}
function getHosts({spec}, join) {
return _.map(spec.rules, y => y.host)
.join(join);
}
function getPaths({spec}, join) {
return _.flatMap(spec.rules, x => x.http.paths)
.map(x => x.path)
.join(join);
}
+21 -4
View File
@@ -12,12 +12,18 @@ import PodsPanel from '../components/podsPanel';
import RamChart from '../components/ramChart';
import SaveButton from '../components/saveButton';
import api from '../services/api';
import {getPodMetrics} from '../utils/metricsHelpers';
import getPodMetrics from '../utils/metricsHelpers';
import {filterByOwner} from '../utils/filterHelper';
import {defaultSortInfo} from '../components/sorter';
const service = api.job;
export default class Job extends Base {
state = {
podsSort: defaultSortInfo(x => this.setState({podsSort: x})),
eventsSort: defaultSortInfo(x => this.setState({eventsSort: x})),
}
componentDidMount() {
const {namespace, name} = this.props;
@@ -31,7 +37,7 @@ export default class Job extends Base {
render() {
const {namespace, name} = this.props;
const {item, pods, events, metrics} = this.state || {};
const {item, pods, events, metrics, podsSort, eventsSort} = this.state;
const filteredPods = filterByOwner(pods, item);
const filteredEvents = filterByOwner(events, item);
@@ -77,8 +83,19 @@ export default class Job extends Base {
</div>
<ContainersPanel spec={item && item.spec.template.spec} />
<PodsPanel items={filteredPods} metrics={filteredMetrics} skipNamespace={true} />
<EventsPanel shortList={true} items={filteredEvents} />
<PodsPanel
items={filteredPods}
sort={podsSort}
metrics={filteredMetrics}
skipNamespace={true}
/>
<EventsPanel
shortList={true}
sort={eventsSort}
items={filteredEvents}
/>
</div>
);
}
+14 -10
View File
@@ -2,6 +2,7 @@ import './logs.scss';
import _ from 'lodash';
import React from 'react';
import Switch from 'react-switch';
import Select from 'react-select';
import Base from '../components/base';
import InputFilter from '../components/inputFilter';
import Loading from '../components/loading';
@@ -71,25 +72,28 @@ export default class Logs extends Base {
render() {
const {namespace, name} = this.props;
const {items, container, containers, filter = '', showPrevious = false} = this.state || {};
const {items, container, containers = [], filter = '', showPrevious = false} = this.state || {};
const lowercaseFilter = filter.toLowerCase();
const filteredLogs = items.filter(x => x.toLowerCase().includes(lowercaseFilter));
const options = containers.map(x => ({value: x, label: x}));
const selected = options.find(x => x.value === container);
return (
<div id='content'>
<div id='header'>
<span className='header_label'>{['Pod Logs', namespace, name].join(' • ')}</span>
<select
value={container}
className='select_namespace'
onChange={x => this.setContainer(x.target.value)}
>
{containers && containers.map(x => (
<option key={x}>{x}</option>
))}
</select>
<div className='select_namespace'>
<Select
className="react-select"
classNamePrefix="react-select"
value={selected}
onChange={x => this.setContainer(x.value)}
options={options}
/>
</div>
<label className='logs_showPrevious'>
<Switch
+23 -19
View File
@@ -1,20 +1,26 @@
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import Sorter, {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import test from '../utils/filterHelper';
export default class Namespaces extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
componentDidMount() {
this.registerApi({
namespaces: api.namespace.list(namespaces => this.setState({namespaces})),
items: api.namespace.list(items => this.setState({items})),
});
}
render() {
const {namespaces, filter = ''} = this.state || {};
const filtered = namespaces && namespaces.filter(x => test(filter, x.metadata.name));
const {items, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
<div id='content'>
@@ -28,24 +34,22 @@ export default class Namespaces extends Base {
<table>
<thead>
<tr>
<MetadataHeaders />
<th>Status</th>
<MetadataHeaders sort={sort} />
<th>
<Sorter field='status.phase' sort={sort}>Status</Sorter>
</th>
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
href={`#/namespace/${x.metadata.name}`}
/>
<td>{x.status.phase}</td>
</tr>
)) : (
<NoResults items={filtered} filter={filter} colSpan='4' />
)}
</tbody>
<TableBody items={filtered} filter={filter} colSpan='4' sort={sort} row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
href={`#/namespace/${x.metadata.name}`}
/>
<td>{x.status.phase}</td>
</tr>
)} />
</table>
</div>
</div>
+12 -2
View File
@@ -8,8 +8,13 @@ import MetadataFields from '../components/metadataFields';
import PodsPanel from '../components/podsPanel';
import {parseCpu, parseRam} from '../utils/unitHelpers';
import Chart from '../components/chart';
import {defaultSortInfo} from '../components/sorter';
export default class Node extends Base {
state = {
podsSort: defaultSortInfo(x => this.setState({podsSort: x})),
};
componentDidMount() {
const {name} = this.props;
@@ -26,7 +31,7 @@ export default class Node extends Base {
render() {
const {name} = this.props;
const {item, pods, metrics, podMetrics} = this.state || {};
const {item, pods, metrics, podMetrics, podsSort} = this.state;
const filteredPods = pods && pods.filter(x => x.spec.nodeName === name);
@@ -74,7 +79,12 @@ export default class Node extends Base {
)}
</div>
<PodsPanel items={filteredPods} metrics={podMetrics} skipNodeName={true} />
<PodsPanel
items={filteredPods}
sort={podsSort}
metrics={podMetrics}
skipNodeName={true}
/>
</div>
);
}
+65 -36
View File
@@ -3,13 +3,18 @@ import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import Chart from '../components/chart';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults, objectMap} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody, objectMap} from '../components/listViewHelpers';
import Sorter, {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import {parseCpu, parseRam} from '../utils/unitHelpers';
import {parseCpu, parseRam, TO_GB, TO_ONE_CPU} from '../utils/unitHelpers';
import test from '../utils/filterHelper';
import {getNodeMetrics, getNodeCpuTotals, getNodeRamTotals} from '../utils/metricsHelpers';
export default class Nodes extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
componentDidMount() {
this.registerApi({
items: api.node.list(items => this.setState({items})),
@@ -18,14 +23,13 @@ export default class Nodes extends Base {
}
render() {
const {items, metrics, filter = ''} = this.state || {};
const {items, metrics, sort, filter} = this.state;
const filtered = items && _.orderBy(items, 'metadata.name')
.filter((x) => {
const labels = x.metadata.labels || {};
const searchableLabels = Object.entries(labels).flat();
return test(filter, x.metadata.name, ...searchableLabels);
});
const filtered = items && items.filter((x) => {
const labels = x.metadata.labels || {};
const searchableLabels = Object.entries(labels).flat();
return test(filter, x.metadata.name, ...searchableLabels);
});
const filteredMetrics = getNodeMetrics(filtered, metrics);
@@ -56,38 +60,27 @@ export default class Nodes extends Base {
<table>
<thead>
<tr>
<MetadataHeaders />
<MetadataHeaders sort={sort} />
<th>Labels</th>
<th>Ready</th>
<th><Sorter field={getReadyStatus} sort={sort}>Ready</Sorter></th>
<th>Cpu</th>
<th>Ram</th>
{/* TODO: support sorting by cpu/ram
<th><Sorter field='' sort={sort}>Cpu</Sorter></th>
<th><Sorter field='' sort={sort}>Ram</Sorter></th> */}
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
href={`#/node/${x.metadata.name}`}
/>
<td>
{objectMap(x.metadata.labels)}
</td>
<td>
{x.status.conditions && (x.status.conditions.find(y => y.type === 'Ready') || {}).status}
</td>
<td>
<CpuChart item={x} metrics={filteredMetrics} />
</td>
<td>
<RamChart item={x} metrics={filteredMetrics} />
</td>
</tr>
)) : (
<NoResults items={filtered} filter={filter} colSpan='8' />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='8' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns item={x} href={`#/node/${x.metadata.name}`} />
<td>{objectMap(x.metadata.labels)}</td>
<td>{getReadyStatus(x)}</td>
<td><CpuChart item={x} metrics={filteredMetrics} /></td>
<td><RamChart item={x} metrics={filteredMetrics} /></td>
</tr>
)} />
</table>
</div>
</div>
@@ -95,6 +88,13 @@ export default class Nodes extends Base {
}
}
function getReadyStatus({status}) {
if (!status.conditions) return null;
const ready = status.conditions.find(y => y.type === 'Ready');
return ready && ready.status;
}
function CpuTotalsChart({items, metrics}) {
const totals = getNodeCpuTotals(items, metrics);
return totals && (
@@ -134,3 +134,32 @@ function RamChart({item, metrics}) {
return (<span>{`${percent}%`}</span>);
}
function getNodeMetrics(nodes, metrics) {
if (!nodes || !metrics) return null;
const names = _.map(nodes, x => x.metadata.name);
const filteredMetrics = metrics.filter(x => names.includes(x.metadata.name));
return _.keyBy(filteredMetrics, 'metadata.name');
}
function getNodeCpuTotals(items, metrics) {
if (!items || !metrics) return null;
const metricValues = Object.values(metrics);
const used = _.sumBy(metricValues, x => parseCpu(x.usage.cpu)) / TO_ONE_CPU;
const available = _.sumBy(items, x => parseCpu(x.status.capacity.cpu)) / TO_ONE_CPU;
return {used, available};
}
function getNodeRamTotals(items, metrics) {
if (!items || !metrics) return null;
const metricValues = Object.values(metrics);
const used = _.sumBy(metricValues, x => parseRam(x.usage.memory)) / TO_GB;
const available = _.sumBy(items, x => parseRam(x.status.capacity.memory)) / TO_GB;
return {used, available};
}
+6 -1
View File
@@ -1,4 +1,9 @@
import React from 'react';
const NotFound = () => <div>Page not found</div>;
const NotFound = () => (
<div id='content'>
<div className='error contentPanel'>Page not found</div>
</div>
);
export default NotFound;
+47 -26
View File
@@ -1,11 +1,18 @@
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import Sorter, {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import test from '../utils/filterHelper';
import {parseDiskSpace} from '../utils/unitHelpers';
export default class PersistentVolumeClaims extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
setNamespace(namespace) {
this.setState({items: null});
@@ -15,7 +22,7 @@ export default class PersistentVolumeClaims extends Base {
}
render() {
const {items, filter = ''} = this.state || {};
const {items, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
@@ -31,36 +38,50 @@ export default class PersistentVolumeClaims extends Base {
<table>
<thead>
<tr>
<MetadataHeaders includeNamespace={true} />
<th>Status</th>
<th>Class Name</th>
<th>Access Modes</th>
<th>Volume</th>
<th>Capacity</th>
<MetadataHeaders includeNamespace={true} sort={sort} />
<th>
<Sorter field='status.phase' sort={sort}>Status</Sorter>
</th>
<th>
<Sorter field='spec.storageClassName' sort={sort}>Class Name</Sorter>
</th>
<th>
<Sorter field={getAccessModes} sort={sort}>Access Modes</Sorter>
</th>
<th>
<Sorter field='spec.volumeName' sort={sort}>Volume</Sorter>
</th>
<th>
<Sorter field={getDiskSpace} sort={sort}>Capacity</Sorter>
</th>
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/persistentvolumeclaim/${x.metadata.namespace}/${x.metadata.name}`}
/>
<td>{x.status.phase}</td>
<td>{x.spec.storageClassName}</td>
<td>{x.spec.accessModes.join(' • ')}</td>
<td>{x.spec.volumeName}</td>
<td>{x.status.capacity && x.status.capacity.storage}</td>
</tr>
)) : (
<NoResults colSpan='9' items={filtered} filter={filter} />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='9' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/persistentvolumeclaim/${x.metadata.namespace}/${x.metadata.name}`}
/>
<td>{x.status.phase}</td>
<td>{x.spec.storageClassName}</td>
<td>{getAccessModes(x, ' • ')}</td>
<td>{x.spec.volumeName}</td>
<td>{x.status.capacity && x.status.capacity.storage}</td>
</tr>
)} />
</table>
</div>
</div>
);
}
}
function getAccessModes({spec}, join = ' ') {
return spec.accessModes.join(join);
}
function getDiskSpace({status}) {
return status.capacity && parseDiskSpace(status.capacity.storage);
}
+31 -22
View File
@@ -1,12 +1,18 @@
import _ from 'lodash';
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import Sorter, {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import test from '../utils/filterHelper';
import {parseDiskSpace} from '../utils/unitHelpers';
export default class PersistentVolumes extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
componentDidMount() {
this.registerApi({
items: api.persistentVolume.list(items => this.setState({items})),
@@ -14,9 +20,8 @@ export default class PersistentVolumes extends Base {
}
render() {
const {items, filter = ''} = this.state || {};
const filtered = items && _.orderBy(items, 'metadata.name')
.filter(x => test(filter, x.metadata.name));
const {items, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
<div id='content'>
@@ -30,29 +35,33 @@ export default class PersistentVolumes extends Base {
<table>
<thead>
<tr>
<MetadataHeaders />
<th>Status</th>
<th>Capacity</th>
<MetadataHeaders sort={sort} />
<th>
<Sorter field='status.phase' sort={sort}>Status</Sorter>
</th>
<th>
<Sorter field={getDiskSpace} sort={sort}>Capacity</Sorter>
</th>
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
href={`#/persistentvolume/${x.metadata.name}`}
/>
<td>{x.status.phase}</td>
<td>{x.spec.capacity && x.spec.capacity.storage}</td>
</tr>
)) : (
<NoResults items={filtered} filter={filter} colSpan='5' />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='5' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
href={`#/persistentvolume/${x.metadata.name}`}
/>
<td>{x.status.phase}</td>
<td>{x.spec.capacity && x.spec.capacity.storage}</td>
</tr>
)} />
</table>
</div>
</div>
);
}
}
function getDiskSpace({spec}) {
return spec.capacity && parseDiskSpace(spec.capacity.storage);
}
+9 -1
View File
@@ -5,8 +5,15 @@ import Filter from '../components/filter';
import api from '../services/api';
import test from '../utils/filterHelper';
import PodsPanel from '../components/podsPanel';
import {defaultSortInfo} from '../components/sorter';
export default class Pods extends Base {
state = {
namespace: '',
filter: '',
sort: defaultSortInfo(this),
};
setNamespace(namespace) {
this.setState({namespace});
this.setState({items: null});
@@ -18,7 +25,7 @@ export default class Pods extends Base {
}
render() {
const {items, metrics, namespace = '', filter = ''} = this.state || {};
const {items, metrics, namespace, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name));
const fixedMetrics = _.keyBy(metrics, 'metadata.name');
const skipNamespace = !!namespace;
@@ -37,6 +44,7 @@ export default class Pods extends Base {
<PodsPanel
items={filtered}
filter={filter}
sort={sort}
metrics={fixedMetrics}
skipNamespace={skipNamespace}
/>
+10 -4
View File
@@ -13,12 +13,18 @@ import PodsPanel from '../components/podsPanel';
import RamChart from '../components/ramChart';
import SaveButton from '../components/saveButton';
import ScaleButton from '../components/scaleButton';
import {getPodMetrics} from '../utils/metricsHelpers';
import {defaultSortInfo} from '../components/sorter';
import getPodMetrics from '../utils/metricsHelpers';
import {filterByOwner} from '../utils/filterHelper';
const service = api.replicaSet;
export default class ReplicaSet extends Base {
state = {
podsSort: defaultSortInfo(x => this.setState({podsSort: x})),
eventsSort: defaultSortInfo(x => this.setState({eventsSort: x})),
};
componentDidMount() {
const {namespace, name} = this.props;
@@ -32,7 +38,7 @@ export default class ReplicaSet extends Base {
render() {
const {namespace, name} = this.props;
const {item, pods, metrics, events} = this.state || {};
const {item, pods, metrics, events, podsSort, eventsSort} = this.state;
const filteredPods = filterByOwner(pods, item);
const filteredEvents = filterByOwner(events, item);
@@ -81,8 +87,8 @@ export default class ReplicaSet extends Base {
</div>
<ContainersPanel spec={item && item.spec.template.spec} />
<PodsPanel items={filteredPods} metrics={filteredMetrics} skipNamespace={true} />
<EventsPanel shortList={true} items={filteredEvents} />
<PodsPanel items={filteredPods} sort={podsSort} metrics={filteredMetrics} skipNamespace={true} />
<EventsPanel shortList={true} sort={eventsSort} items={filteredEvents} />
</div>
);
}
+8 -2
View File
@@ -2,10 +2,16 @@ import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import ReplicaSetsPanel from '../components/replicaSetsPanel';
import {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import test from '../utils/filterHelper';
export default class ReplicaSets extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
setNamespace(namespace) {
this.setState({items: null});
@@ -15,7 +21,7 @@ export default class ReplicaSets extends Base {
}
render() {
const {items, filter = ''} = this.state || {};
const {items, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
@@ -27,7 +33,7 @@ export default class ReplicaSets extends Base {
onNamespaceChange={x => this.setNamespace(x)}
/>
<ReplicaSetsPanel items={filtered} filter={filter} />
<ReplicaSetsPanel items={filtered} sort={sort} filter={filter} />
</div>
);
}
+9 -13
View File
@@ -5,7 +5,7 @@ import api from '../services/api';
import ItemHeader from '../components/itemHeader';
import Loading from '../components/loading';
import MetadataFields from '../components/metadataFields';
import {NoResults, hasResults} from '../components/listViewHelpers';
import {TableBody} from '../components/listViewHelpers';
import SaveButton from '../components/saveButton';
import DeleteButton from '../components/deleteButton';
@@ -59,18 +59,14 @@ export default class Role extends Base {
</tr>
</thead>
<tbody>
{hasResults(rules) ? rules.map((x, i) => (
<tr key={i}>
<td>{_.map(x.resources, toDiv)}</td>
<td>{_.map(x.resourceNames, toDiv)}</td>
<td>{_.map(x.verbs, toDiv)}</td>
<td>{_.map(x.apiGroups, toDiv)}</td>
</tr>
)) : (
<NoResults items={rules} colSpan='4' />
)}
</tbody>
<TableBody items={rules} colSpan='4' row={(x, i) => (
<tr key={i}>
<td>{_.map(x.resources, toDiv)}</td>
<td>{_.map(x.resourceNames, toDiv)}</td>
<td>{_.map(x.verbs, toDiv)}</td>
<td>{_.map(x.apiGroups, toDiv)}</td>
</tr>
)} />
</table>
</div>
</div>
+24 -23
View File
@@ -5,14 +5,19 @@ import ItemHeader from '../components/itemHeader';
import Loading from '../components/loading';
import Field from '../components/field';
import MetadataFields from '../components/metadataFields';
import {NoResults, hasResults} from '../components/listViewHelpers';
import {TableBody} from '../components/listViewHelpers';
import SaveButton from '../components/saveButton';
import DeleteButton from '../components/deleteButton';
import Sorter, {defaultSortInfo} from '../components/sorter';
import ResourceSvg from '../art/resourceSvg';
const service = api.roleBinding;
export default class RoleBinding extends Base {
state = {
sort: defaultSortInfo(this),
};
componentDidMount() {
const {namespace, name} = this.props;
@@ -23,7 +28,7 @@ export default class RoleBinding extends Base {
render() {
const {namespace, name} = this.props;
const {item} = this.state || {};
const {item, sort} = this.state || {};
const subjects = item && item.subjects;
return (
@@ -58,30 +63,26 @@ export default class RoleBinding extends Base {
<table>
<thead>
<tr>
<th className='th_icon'></th>
<th>Name</th>
<th>Namespace</th>
<th>Api Group</th>
<th className='th_icon'><Sorter filed='kind' sort={sort}>Type</Sorter></th>
<th><Sorter field='name' sort={sort}>Name</Sorter></th>
<th><Sorter field='namespace' sort={sort}>Namespace</Sorter></th>
<th><Sorter field='apiGroup' sort={sort}>Api Group</Sorter></th>
</tr>
</thead>
<tbody>
{hasResults(subjects) ? subjects.map(x => (
<tr key={x.name}>
<td>
<ResourceSvg resource={x.kind} />
<div className='td_iconLabel'>{x.kind}</div>
</td>
<td>
{x.kind === 'ServiceAccount' ? (<a href={getSubjectHref(x)}>{x.name}</a>) : x.name}
</td>
<td>{x.namespace}</td>
<td>{x.apiGroup}</td>
</tr>
)) : (
<NoResults items={subjects} colSpan='4' />
)}
</tbody>
<TableBody items={subjects} sort={sort} colSpan='4' row={x => (
<tr key={x.name}>
<td>
<ResourceSvg resource={x.kind} />
<div className='td_iconLabel'>{x.kind}</div>
</td>
<td>
{x.kind === 'ServiceAccount' ? (<a href={getSubjectHref(x)}>{x.name}</a>) : x.name}
</td>
<td>{x.namespace}</td>
<td>{x.apiGroup}</td>
</tr>
)} />
</table>
</div>
</div>
+21 -20
View File
@@ -1,12 +1,17 @@
import _ from 'lodash';
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import test from '../utils/filterHelper';
export default class RoleBindings extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
componentDidMount() {
this.registerApi({
role: api.roleBinding.list(null, x => this.setState({clusterRoles: x})),
@@ -15,14 +20,14 @@ export default class RoleBindings extends Base {
}
render() {
const {clusterRoles, clusterRoleBindings, filter = ''} = this.state || {};
const {clusterRoles, clusterRoleBindings, sort, filter} = this.state;
let items;
if (clusterRoles || clusterRoleBindings) {
items = [...clusterRoles || [], ...clusterRoleBindings || []];
}
const filtered = items && _.sortBy(items, 'metadata.name').filter(x => test(filter, x.metadata.name));
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
<div id='content'>
@@ -36,25 +41,21 @@ export default class RoleBindings extends Base {
<table>
<thead>
<tr>
<MetadataHeaders includeNamespace={true} />
<MetadataHeaders sort={sort} includeNamespace={true} />
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={x.kind === 'ClusterRoleBinding'
? `#/clusterrolebinding/${x.metadata.name}`
: `#/rolebinding/${x.metadata.namespace}/${x.metadata.name}`}
/>
</tr>
)) : (
<NoResults items={filtered} filter={filter} colSpan='4' />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='4' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={x.kind === 'ClusterRoleBinding'
? `#/clusterrolebinding/${x.metadata.name}`
: `#/rolebinding/${x.metadata.namespace}/${x.metadata.name}`}
/>
</tr>
)} />
</table>
</div>
</div>
+21 -20
View File
@@ -1,12 +1,17 @@
import _ from 'lodash';
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import test from '../utils/filterHelper';
export default class Roles extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
componentDidMount() {
this.registerApi({
role: api.role.list(null, roles => this.setState({roles})),
@@ -15,14 +20,14 @@ export default class Roles extends Base {
}
render() {
const {roles, clusterRoles, filter = ''} = this.state || {};
const {roles, clusterRoles, sort, filter} = this.state;
let items;
if (roles || clusterRoles) {
items = [...roles || [], ...clusterRoles || []];
}
const filtered = items && _.sortBy(items, 'metadata.name').filter(x => test(filter, x.metadata.name));
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
<div id='content'>
@@ -36,25 +41,21 @@ export default class Roles extends Base {
<table>
<thead>
<tr>
<MetadataHeaders includeNamespace={true} />
<MetadataHeaders sort={sort} includeNamespace={true} />
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={x.kind === 'ClusterRole'
? `#/clusterrole/${x.metadata.name}`
: `#/role/${x.metadata.namespace}/${x.metadata.name}`}
/>
</tr>
)) : (
<NoResults items={filtered} filter={filter} colSpan='4' />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='4' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={x.kind === 'ClusterRole'
? `#/clusterrole/${x.metadata.name}`
: `#/role/${x.metadata.namespace}/${x.metadata.name}`}
/>
</tr>
)} />
</table>
</div>
</div>
+20 -18
View File
@@ -1,11 +1,17 @@
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import Sorter, {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import test from '../utils/filterHelper';
export default class Secrets extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
setNamespace(namespace) {
this.setState({items: null});
@@ -15,7 +21,7 @@ export default class Secrets extends Base {
}
render() {
const {items, filter = ''} = this.state || {};
const {items, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
@@ -31,25 +37,21 @@ export default class Secrets extends Base {
<table>
<thead>
<tr>
<MetadataHeaders includeNamespace={true} />
<th>Type</th>
<MetadataHeaders sort={sort} includeNamespace={true} />
<th><Sorter field='type' sort={sort}>Type</Sorter></th>
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/secret/${x.metadata.namespace}/${x.metadata.name}`}
/>
<td>{x.type}</td>
</tr>
)) : (
<NoResults colSpan='5' items={filtered} filter={filter} />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='5' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/secret/${x.metadata.namespace}/${x.metadata.name}`}
/>
<td>{x.type}</td>
</tr>
)} />
</table>
</div>
</div>
+9 -14
View File
@@ -1,8 +1,7 @@
import _ from 'lodash';
import React from 'react';
import api from '../services/api';
import EventsPanel from '../components/eventsPanel';
import {NoResults, hasResults} from '../components/listViewHelpers';
import {TableBody} from '../components/listViewHelpers';
import Base from '../components/base';
import DeleteButton from '../components/deleteButton';
import Field from '../components/field';
@@ -65,18 +64,14 @@ export default class Service extends Base {
</tr>
</thead>
<tbody>
{hasResults(ports) ? _.map(item.spec.ports, x => (
<tr key={x.port}>
<td>{x.name}</td>
<td>{x.port}</td>
<td>{x.protocol}</td>
<td>{x.targetPort}</td>
</tr>
)) : (
<NoResults colSpan='4' items={ports} />
)}
</tbody>
<TableBody items={ports} colSpan='4' row={x => (
<tr key={x.port}>
<td>{x.name}</td>
<td>{x.port}</td>
<td>{x.protocol}</td>
<td>{x.targetPort}</td>
</tr>
)} />
</table>
</Field>
</div>
+18 -16
View File
@@ -1,11 +1,17 @@
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import test from '../utils/filterHelper';
export default class ServiceAccounts extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
setNamespace(namespace) {
this.setState({items: null});
@@ -15,7 +21,7 @@ export default class ServiceAccounts extends Base {
}
render() {
const {items, filter = ''} = this.state || {};
const {items, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
@@ -31,23 +37,19 @@ export default class ServiceAccounts extends Base {
<table>
<thead>
<tr>
<MetadataHeaders includeNamespace={true} />
<MetadataHeaders sort={sort} includeNamespace={true} />
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/serviceaccount/${x.metadata.namespace}/${x.metadata.name}`}
/>
</tr>
)) : (
<NoResults colSpan='9' items={filtered} filter={filter} />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='9' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/serviceaccount/${x.metadata.namespace}/${x.metadata.name}`}
/>
</tr>
)} />
</table>
</div>
</div>
+18 -16
View File
@@ -1,11 +1,17 @@
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import test from '../utils/filterHelper';
export default class Services extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
setNamespace(namespace) {
this.setState({items: null});
@@ -15,7 +21,7 @@ export default class Services extends Base {
}
render() {
const {items, filter = ''} = this.state || {};
const {items, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
@@ -31,23 +37,19 @@ export default class Services extends Base {
<table>
<thead>
<tr>
<MetadataHeaders includeNamespace={true} />
<MetadataHeaders sort={sort} includeNamespace={true} />
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/service/${x.metadata.namespace}/${x.metadata.name}`}
/>
</tr>
)) : (
<NoResults colSpan='4' items={filtered} filter={filter} />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='4' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
includeNamespace={true}
href={`#/service/${x.metadata.namespace}/${x.metadata.name}`}
/>
</tr>
)} />
</table>
</div>
</div>
+21 -4
View File
@@ -12,13 +12,19 @@ import EventsPanel from '../components/eventsPanel';
import Chart from '../components/chart';
import RamChart from '../components/ramChart';
import CpuChart from '../components/cpuChart';
import {getPodMetrics} from '../utils/metricsHelpers';
import getPodMetrics from '../utils/metricsHelpers';
import {filterByOwner} from '../utils/filterHelper';
import ContainersPanel from '../components/containersPanel';
import {defaultSortInfo} from '../components/sorter';
const service = api.statefulSet;
export default class StatefulSet extends Base {
state = {
podsSort: defaultSortInfo(x => this.setState({podsSort: x})),
eventsSort: defaultSortInfo(x => this.setState({eventsSort: x})),
};
componentDidMount() {
const {namespace, name} = this.props;
@@ -32,7 +38,7 @@ export default class StatefulSet extends Base {
render() {
const {namespace, name} = this.props;
const {item, pods, events, metrics} = this.state || {};
const {item, pods, events, metrics, podsSort, eventsSort} = this.state;
const filteredPods = filterByOwner(pods, item);
const filteredEvents = filterByOwner(events, item);
@@ -80,8 +86,19 @@ export default class StatefulSet extends Base {
</div>
<ContainersPanel spec={item && item.spec.template.spec} />
<PodsPanel items={filteredPods} metrics={filteredMetrics} skipNamespace={true} />
<EventsPanel shortList={true} items={filteredEvents} />
<PodsPanel
items={filteredPods}
sort={podsSort}
metrics={filteredMetrics}
skipNamespace={true}
/>
<EventsPanel
shortList={true}
sort={eventsSort}
items={filteredEvents}
/>
</div>
);
}
+21 -19
View File
@@ -1,11 +1,17 @@
import React from 'react';
import Base from '../components/base';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import api from '../services/api';
import test from '../utils/filterHelper';
import Sorter, {defaultSortInfo} from '../components/sorter';
export default class StorageClasses extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
componentDidMount() {
this.registerApi({
items: api.storageClass.list(items => this.setState({items})),
@@ -13,7 +19,7 @@ export default class StorageClasses extends Base {
}
render() {
const {items, filter = ''} = this.state || {};
const {items, sort, filter} = this.state;
const filtered = items && items.filter(x => test(filter, x.metadata.name));
return (
@@ -28,26 +34,22 @@ export default class StorageClasses extends Base {
<table>
<thead>
<tr>
<MetadataHeaders />
<th>Reclaim Policy</th>
<th>Mode</th>
<MetadataHeaders sort={sort} />
<th><Sorter field='reclaimPolicy' sort={sort}>Reclaim Policy</Sorter></th>
<th><Sorter field='volumeBindingMode' sort={sort}>Mode</Sorter></th>
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
href={`#/storageclass/${x.metadata.name}`}
/>
<td>{x.reclaimPolicy}</td>
<td>{x.volumeBindingMode}</td>
</tr>
)) : (
<NoResults items={filtered} filter={filter} colSpan='5' />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='5' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
href={`#/storageclass/${x.metadata.name}`}
/>
<td>{x.reclaimPolicy}</td>
<td>{x.volumeBindingMode}</td>
</tr>
)} />
</table>
</div>
</div>
+23 -26
View File
@@ -3,12 +3,18 @@ import React from 'react';
import Base from '../components/base';
import Chart from '../components/chart';
import Filter from '../components/filter';
import {MetadataHeaders, MetadataColumns, NoResults, hasResults} from '../components/listViewHelpers';
import {MetadataHeaders, MetadataColumns, TableBody} from '../components/listViewHelpers';
import Sorter, {defaultSortInfo} from '../components/sorter';
import api from '../services/api';
import test from '../utils/filterHelper';
import Working from '../components/working';
export default class Workloads extends Base {
state = {
filter: '',
sort: defaultSortInfo(this),
};
setNamespace(namespace) {
this.setState({
cronJobs: null,
@@ -32,10 +38,10 @@ export default class Workloads extends Base {
}
render() {
const {cronJobs, daemonSets, deployments, jobs, statefulSets, filter = '', sortBy, sortDirection} = this.state || {};
const {cronJobs, daemonSets, deployments, jobs, statefulSets, sort, filter} = this.state;
const items = [cronJobs, daemonSets, deployments, jobs, statefulSets];
const filtered = filterControllers(filter, items, sortBy, sortDirection);
const filtered = filterControllers(filter, items);
const controllerCount = filtered && filtered.length;
const pendingControllerCount = getPendingControllerCount(filtered);
@@ -75,30 +81,22 @@ export default class Workloads extends Base {
<table>
<thead>
<tr>
<MetadataHeaders
sortBy={sortBy}
sortDirection={sortDirection}
onSort={(x, y) => this.setState({sortBy: x, sortDirection: y})}
/>
<th>Pods</th>
<MetadataHeaders sort={sort}/>
<th><Sorter field={getExpectedCount} sort={sort}>Pods</Sorter></th>
</tr>
</thead>
<tbody>
{hasResults(filtered) ? filtered.map(x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
href={`#/workload/${x.kind.toLowerCase()}/${x.metadata.namespace}/${x.metadata.name}`}
/>
<td>
<Status item={x} />
</td>
</tr>
)) : (
<NoResults items={filtered} filter={filter} colSpan='4' />
)}
</tbody>
<TableBody items={filtered} filter={filter} sort={sort} colSpan='4' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns
item={x}
href={`#/workload/${x.kind.toLowerCase()}/${x.metadata.namespace}/${x.metadata.name}`}
/>
<td>
<Status item={x} />
</td>
</tr>
)} />
</table>
</div>
</div>
@@ -132,14 +130,13 @@ function getExpectedCount({spec, status}) {
return spec.replicas || status.currentNumberScheduled || 0;
}
function filterControllers(filter, items, sortBy, sortDirection) {
function filterControllers(filter, items) {
const results = items.flat();
if (results.length && items.some(x => !x)) return null;
return _(results)
.flatten()
.orderBy([sortBy], [sortDirection])
.filter(x => test(filter, x.metadata.name))
.value();
}