From 3a2459e771791e6ffdf38d1e93187a5c43eb4942 Mon Sep 17 00:00:00 2001 From: Tomasz Adamski Date: Sat, 13 Jan 2018 18:48:03 +0100 Subject: [PATCH] Labels filtering (#154) * filtering by labels * a little bit of polishing * user-guide doc update --- app/src/app.js | 124 +++++++++++++++++++++++++++----------------- docs/user-guide.rst | 9 +++- 2 files changed, 84 insertions(+), 49 deletions(-) diff --git a/app/src/app.js b/app/src/app.js index c22b16a..6424da0 100644 --- a/app/src/app.js +++ b/app/src/app.js @@ -2,9 +2,9 @@ import Tooltip from './tooltip.js' import Cluster from './cluster.js' import {Pod, ALL_PODS, sortByName, sortByMemory, sortByCPU, sortByAge} from './pod.js' import SelectBox from './selectbox' -import { Theme, ALL_THEMES} from './themes.js' -import { DESATURATION_FILTER } from './filters.js' -import { JSON_delta } from './vendor/json_delta.js' +import {Theme, ALL_THEMES} from './themes.js' +import {DESATURATION_FILTER} from './filters.js' +import {JSON_delta} from './vendor/json_delta.js' import Config from './config.js' const PIXI = require('pixi.js') @@ -17,7 +17,7 @@ export default class App { constructor() { const params = this.parseLocationHash() this.config = Config.fromParams(params) - this.filterString = params.get('q') || '' + this.filterString = (params.get('q') && decodeURIComponent(params.get('q'))) || '' this.selectedClusters = new Set((params.get('clusters') || '').split(',').filter(x => x)) this.seenPods = new Set() this.sorterFn = '' @@ -50,13 +50,32 @@ export default class App { const pairs = [] for (const [key, value] of params) { if (value) { - pairs.push(key + '=' + value) + pairs.push(key + '=' + encodeURIComponent(value)) } } document.location.hash = '#' + pairs.sort().join(';') } + nameMatches(pod, searchString) { + const name = pod.name + return name && name.includes(searchString) + } + + labelMatches(pod, name, value) { + const labels = pod.labels + return labels && labels[name] === value + } + + createMatchesFunctionForQuery(query) { + if (query.includes('=')) { + const labelAndValue = query.split('=', 2) + return pod => this.labelMatches(pod, labelAndValue[0], labelAndValue[1]) + } else { + return pod => this.nameMatches(pod, query) + } + } + filter() { const searchString = this.filterString if (this.searchText) { @@ -64,32 +83,29 @@ export default class App { this.searchText.text = searchString } this.changeLocationHash('q', searchString) - const filter = DESATURATION_FILTER + const elementDisplayFilter = DESATURATION_FILTER + const filterableElements = [] + const matchesQuery = this.createMatchesFunctionForQuery(searchString) for (const cluster of this.viewContainer.children) { for (const node of cluster.children) { - const name = node.pod && node.pod.name - if (name) { - // node is actually unassigned pod - if (!name.includes(searchString)){ - node.filters = [filter] - } else { - // TODO: pod might have other filters set.. - node.filters = [] - } + if (node.pod) { // node is actually unassigned pod + filterableElements.push(node) } for (const pod of node.children) { - const name = pod.pod && pod.pod.name - if (name) { - if (!name.includes(searchString)) { - pod.filters = [filter] - } else { - // TODO: pod might have other filters set.. - pod.filters = [] - } + if (pod.pod) { + filterableElements.push(pod) } } } } + filterableElements.forEach(value => { + if (!matchesQuery(value.pod)) { + value.filters = [elementDisplayFilter] + } else { + // TODO: pod might have other filters set.. + value.filters = [] + } + }) } initialize() { @@ -102,7 +118,7 @@ export default class App { renderer.autoResize = true renderer.resize(window.innerWidth, window.innerHeight) - window.onresize = function() { + window.onresize = function () { renderer.resize(window.innerWidth, window.innerHeight) } @@ -117,7 +133,9 @@ export default class App { setInterval(this.pruneUnavailableClusters.bind(this), 5 * 1000) if (this.config.reloadIntervalSeconds) { - setTimeout(function() { location.reload(false) }, this.config.reloadIntervalSeconds * 1000) + setTimeout(function () { + location.reload(false) + }, this.config.reloadIntervalSeconds * 1000) } } @@ -163,7 +181,8 @@ export default class App { function mouseDownHandler(event) { if (event.button == 0 || event.button == 1) { - prevX = event.clientX; prevY = event.clientY + prevX = event.clientX + prevY = event.clientY isDragging = true this.renderer.view.style.cursor = 'move' } @@ -181,7 +200,8 @@ export default class App { // stop any current move animation this.viewContainerTargetPosition.x = this.viewContainer.x this.viewContainerTargetPosition.y = this.viewContainer.y - prevX = event.clientX; prevY = event.clientY + prevX = event.clientX + prevY = event.clientY } function mouseUpHandler(_event) { @@ -192,7 +212,8 @@ export default class App { function touchStartHandler(event) { if (event.touches.length == 1) { const touch = event.touches[0] - prevX = touch.clientX; prevY = touch.clientY + prevX = touch.clientX + prevY = touch.clientY isDragging = true } } @@ -211,7 +232,8 @@ export default class App { // stop any current move animation this.viewContainerTargetPosition.x = this.viewContainer.x this.viewContainerTargetPosition.y = this.viewContainer.y - prevX = touch.clientX; prevY = touch.clientY + prevX = touch.clientX + prevY = touch.clientY } } @@ -234,7 +256,7 @@ export default class App { return interactionObj.getLocalPosition(that.viewContainer, undefined, {x: x, y: y}) } - const minScale = 1/32 + const minScale = 1 / 32 const maxScale = 32 function zoom(x, y, isZoomIn) { @@ -272,7 +294,11 @@ export default class App { menuBar.drawRect(20, 3, 200, 22) this.stage.addChild(menuBar) - const searchPrompt = new PIXI.Text('>', {fontFamily: 'ShareTechMono', fontSize: 14, fill: this.theme.primaryColor}) + const searchPrompt = new PIXI.Text('>', { + fontFamily: 'ShareTechMono', + fontSize: 14, + fill: this.theme.primaryColor + }) searchPrompt.x = 26 searchPrompt.y = 8 PIXI.ticker.shared.add(function (_) { @@ -303,15 +329,17 @@ export default class App { //setting default sort this.sorterFn = items[0].value const app = this - const selectBox = new SelectBox(items, this.sorterFn, function(value) { + const selectBox = new SelectBox(items, this.sorterFn, function (value) { app.changeSorting(value) }) selectBox.x = 265 selectBox.y = 3 menuBar.addChild(selectBox.draw()) - const themeOptions = Object.keys(ALL_THEMES).sort().map(name => { return {text: name.toUpperCase(), value: name}}) - const themeSelector = new SelectBox(themeOptions, this.theme.name, function(value) { + const themeOptions = Object.keys(ALL_THEMES).sort().map(name => { + return {text: name.toUpperCase(), value: name} + }) + const themeSelector = new SelectBox(themeOptions, this.theme.name, function (value) { app.switchTheme(value) }) themeSelector.x = 420 @@ -351,7 +379,7 @@ export default class App { pod.blendMode = PIXI.BLEND_MODES.ADD pod.interactive = false const targetPosition = globalPosition - const angle = Math.random()*Math.PI*2 + const angle = Math.random() * Math.PI * 2 const cos = Math.cos(angle) const sin = Math.sin(angle) const distance = Math.max(200, Math.random() * Math.min(this.renderer.width, this.renderer.height)) @@ -385,11 +413,12 @@ export default class App { PIXI.ticker.shared.add(tick) this.stage.addChild(pod) } + animatePodDeletion(originalPod, globalPosition) { const pod = new Pod(originalPod.pod, null, this.tooltip) pod.draw() pod.blendMode = PIXI.BLEND_MODES.ADD - const globalCenter = new PIXI.Point(globalPosition.x + pod.width/2, globalPosition.y + pod.height/2) + const globalCenter = new PIXI.Point(globalPosition.x + pod.width / 2, globalPosition.y + pod.height / 2) const blur = new PIXI.filters.BlurFilter(4) pod.filters = [blur] pod.position = globalPosition.clone() @@ -397,14 +426,14 @@ export default class App { pod._progress = 1 originalPod.destroy() const that = this - const tick = function(t) { + const tick = function (t) { // progress goes from 1 to 0 const progress = Math.max(0, pod._progress - (0.02 * t)) const scale = 1 + ((1 - progress) * 8) pod._progress = progress pod.alpha = progress pod.scale.set(scale) - pod.position.set(globalCenter.x - pod.width/2, globalCenter.y - pod.height/2) + pod.position.set(globalCenter.x - pod.width / 2, globalCenter.y - pod.height / 2) if (progress <= 0) { PIXI.ticker.shared.remove(tick) @@ -415,6 +444,7 @@ export default class App { PIXI.ticker.shared.add(tick) this.stage.addChild(pod) } + update() { // make sure we create a copy (this.clusters might get modified) const clusters = Array.from(this.clusters.entries()).sort().map(idCluster => idCluster[1]) @@ -441,7 +471,7 @@ export default class App { // NOTE: we need to do this BEFORE removeChildren() // to get correct global coordinates const globalPos = pod.toGlobal({x: 0, y: 0}) - window.setTimeout(function() { + window.setTimeout(function () { that.animatePodDeletion(pod, globalPos) }, 100 * changes) } else { @@ -488,7 +518,7 @@ export default class App { this.seenPods.add(key) if (!this.bootstrapping && changes < 10) { const globalPos = pod.toGlobal({x: 0, y: 0}) - window.setTimeout(function() { + window.setTimeout(function () { that.animatePodCreation(pod, globalPos) }, 100 * changes) } @@ -505,10 +535,10 @@ export default class App { this.viewContainer.position.y = this.viewContainerTargetPosition.y } else { if (Math.abs(deltaX) > time) { - this.viewContainer.x += time * Math.sign(deltaX) * Math.max(10, Math.abs(deltaX)/10) + this.viewContainer.x += time * Math.sign(deltaX) * Math.max(10, Math.abs(deltaX) / 10) } if (Math.abs(deltaY) > time) { - this.viewContainer.y += time * Math.sign(deltaY) * Math.max(10, Math.abs(deltaY)/10) + this.viewContainer.y += time * Math.sign(deltaY) * Math.max(10, Math.abs(deltaY) / 10) } } this.renderer.render(this.stage) @@ -598,7 +628,7 @@ export default class App { } const eventSource = this.eventSource = new EventSource(url, {credentials: 'include'}) this.keepAlive() - eventSource.onerror = function(_event) { + eventSource.onerror = function (_event) { that._errors++ if (that._errors <= 1) { // immediately reconnect on first error @@ -608,7 +638,7 @@ export default class App { that.disconnect() } } - eventSource.addEventListener('clusterupdate', function(event) { + eventSource.addEventListener('clusterupdate', function (event) { that._errors = 0 that.keepAlive() const cluster = JSON.parse(event.data) @@ -621,7 +651,7 @@ export default class App { that.update() } }) - eventSource.addEventListener('clusterdelta', function(event) { + eventSource.addEventListener('clusterdelta', function (event) { that._errors = 0 that.keepAlive() const data = JSON.parse(event.data) @@ -636,13 +666,13 @@ export default class App { that.update() } }) - eventSource.addEventListener('clusterstatus', function(event) { + eventSource.addEventListener('clusterstatus', function (event) { that._errors = 0 that.keepAlive() const data = JSON.parse(event.data) that.clusterStatuses.set(data.cluster_id, data.status) }) - eventSource.addEventListener('bootstrapend', function(_event) { + eventSource.addEventListener('bootstrapend', function (_event) { that._errors = 0 that.keepAlive() that.bootstrapping = false diff --git a/docs/user-guide.rst b/docs/user-guide.rst index 526c9f0..46ea1aa 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -24,12 +24,17 @@ Various UI elements provide additional tooltip information when hovering over th * Hovering over a pod will show the pod's labels, container status and resources. -Filtering Pods by Name +Filtering Pods ====================== Kubernetes Operational View allows you to quickly find your running application pods. -Typing characters will filter pods by name, i.e. non-matching pods will be greyed out. +Typing characters will run the filter, i.e. non-matching pods will be greyed out. + +You can filter by: + +* name +* labels - when query includes ``=``, e.g. ``env=prod`` The pod filter is persisted in the location bar (``#q=..`` query parameter) which allows to conveniently send the filtered view to other users (e.g. for troubleshooting).