From 4ec8d230db48c4ce973f89ab98b0de27c57fa387 Mon Sep 17 00:00:00 2001 From: Paul Richardson Date: Mon, 23 Sep 2024 15:56:14 +0100 Subject: [PATCH] Change filtering to push down into the mgmt and k8 services (#496) * namespace-client.ts * Change name of 2nd callback parameter to fullPodCount to make it clear this is the total jolokia pods after filtering but before page slicing * Adds filtering and sorting getters/setters to affect the pods returned by the pod watchers * If any change occurs from filtering or sorting then the callback should be called in order to notify the UI * filter.ts * Refactors the TypeFilter to be a single class containing both namespace and pod name values * kubernetes-service.ts * Add/remove the pod projects to/from the service depending on if there are pods returned after filtering * management-service.ts * Add/remove the mgmt pod projects to/from the service depending on if there are pods returned after filtering * Discover context * Use only 1 filter rather than an array * Breaks out the empty state into its own component for use in other parts of the Discover page * DiscoverProjectContent.ts * Needs refresh Effect to reset the pagination index if it is greater than the project count returned * DiscoverToolbar.ts * When the buttons are clicked call the management service functions for filtering and sorting so that all pod projects down to the k8 service are effected. * discover-service.ts * Only responsible for grouping pods rather as filtering is re-assigned to other services --- .../src/client/namespace-client.ts | 165 ++++++++++++++-- packages/kubernetes-api/src/filter.ts | 75 ++++++++ packages/kubernetes-api/src/globals.ts | 2 +- packages/kubernetes-api/src/index.ts | 2 + .../kubernetes-api/src/kubernetes-service.ts | 97 ++++++++-- packages/kubernetes-api/src/sort.ts | 4 + packages/management-api/src/globals.ts | 21 -- packages/management-api/src/managed-pod.ts | 22 +-- .../management-api/src/managed-project.ts | 10 +- .../management-api/src/management-service.ts | 49 ++++- .../online-shell/src/discover/Discover.tsx | 27 +-- .../src/discover/DiscoverEmptyContent.tsx | 44 +++++ .../src/discover/DiscoverProjectContent.tsx | 23 ++- .../src/discover/DiscoverToolbar.tsx | 182 ++++++++++-------- packages/online-shell/src/discover/context.ts | 21 +- .../src/discover/discover-project.ts | 26 +-- .../src/discover/discover-service.ts | 90 ++------- 17 files changed, 566 insertions(+), 294 deletions(-) create mode 100644 packages/kubernetes-api/src/filter.ts create mode 100644 packages/kubernetes-api/src/sort.ts create mode 100644 packages/online-shell/src/discover/DiscoverEmptyContent.tsx diff --git a/packages/kubernetes-api/src/client/namespace-client.ts b/packages/kubernetes-api/src/client/namespace-client.ts index 7902c2dd..167c79ee 100644 --- a/packages/kubernetes-api/src/client/namespace-client.ts +++ b/packages/kubernetes-api/src/client/namespace-client.ts @@ -1,11 +1,13 @@ import jsonpath from 'jsonpath' -import { JOLOKIA_PORT_QUERY, KubeObject, KubePod, Paging } from '../globals' +import { JOLOKIA_PORT_QUERY, KubeObject, KubePod, Paging, log } from '../globals' +import { TypeFilter } from '../filter' import { WatchTypes } from '../model' import { isObject } from '../utils' +import { SortOrder } from '../sort' import { Watched, KOptions, ProcessDataCallback } from './globals' import { clientFactory } from './client-factory' -export type NamespaceClientCallback = (jolokiaPods: KubePod[], jolokiaTotal: number, error?: Error) => void +export type NamespaceClientCallback = (jolokiaPods: KubePod[], fullPodCount: number, error?: Error) => void export interface Client { watched: Watched @@ -23,11 +25,17 @@ interface PodWatchers { export class NamespaceClient implements Paging { private _current = 0 + private _podList: Set = new Set() + private _filteredList: string[] = [] + private _notifyChange: boolean = false + private _podWatchers: PodWatchers = {} private _nsWatcher?: Client private _refreshing = 0 private _limit = 3 + private _sortOrder?: SortOrder + private _typeFilter?: TypeFilter constructor( private _namespace: string, @@ -44,6 +52,12 @@ export class NamespaceClient implements Paging { this._callback(this.getJolokiaPods(), this._podList.size, cbError) } + private isFiltered(): boolean { + if (!this._typeFilter) return false + + return !this._typeFilter.filterNS(this._namespace) + } + private initPodOptions(kind: string, name?: string): KOptions { const podOptions: KOptions = { kind: kind, @@ -58,24 +72,75 @@ export class NamespaceClient implements Paging { } private createPodWatchers() { - if (this._podList.size === 0 || this._current >= this._podList.size) return + log.debug(`[NamespaceClient ${this._namespace}]: creating pod watchers`) + + if (this._podList.size === 0 || this._current >= this._podList.size) { + log.debug(`[NamespaceClient ${this._namespace}]: no pods in namespace`) + this._callback(this.getJolokiaPods(), 0) + return + } + + const sortFn = (a: string, b: string) => { + const order = this._sortOrder === SortOrder.DESC ? -1 : 1 + return a.localeCompare(b) * order + } + + /* + * Sort the pods according to the supplied sort order + * then selects the block of pods determined by current and limit + */ + log.debug(`[NamespaceClient ${this._namespace}]: sort order: `, this._sortOrder, ' filter: ', this._typeFilter) + + let pagedPods: string[] + if (this.isFiltered()) { + // Filtered out by namespace + this._filteredList = [] + pagedPods = [] + } else { + /* + * Filter out pods based on pod name and then sort + */ + log.debug(`[NamespaceClient ${this._namespace}]: has pods`, this._podList) + + this._filteredList = Array.from(this._podList) + .filter(podName => (!this._typeFilter) ? true : this._typeFilter.filterPod(podName)) + .sort(sortFn) - const podNames = Array.from(this._podList) - .sort() - .slice(this._current, this._current + this._limit) + log.debug(`[NamespaceClient ${this._namespace}]: pods after filtering`, this._podList) - // Remove watchers for pods not in the slice of the sorted list + if (this._current >= this._filteredList.length) { + // if current is bigger than filtered podNames list then + // reset it back to 0 + this._current = 0 + } + + // Set the paged pods list base on current and limit + pagedPods = this._filteredList + .slice(this._current, this._current + this._limit) + } + + log.debug(`[NamespaceClient ${this._namespace}]: pods to be watched`, pagedPods) + + /* + * Remove watchers for pods not in the slice of the filtered/sorted list + */ Object.entries(this._podWatchers) - .filter(([name, _]) => { - return !podNames.includes(name) - }) + .filter(([name, _]) => !pagedPods.includes(name)) .forEach(([name, podWatcher]) => { + log.debug(`[NamespaceClient ${this._namespace}]: deleting pod watcher [${name}]`) + clientFactory.destroy(podWatcher.client.watched, podWatcher.client.watch) delete this._podWatchers[name] + this._notifyChange = true }) - this._refreshing = podNames.length - podNames.forEach(name => { + log.debug(`[NamespaceClient ${this._namespace}]: pod watchers already initialised`, this._podWatchers) + + /* + * Create any new pod watchers + */ + this._refreshing = pagedPods.length + pagedPods.forEach(name => { // Already watching this pod if (isObject(this._podWatchers[name])) { this._refreshing-- @@ -87,17 +152,23 @@ export class NamespaceClient implements Paging { const _podWatcher = _podClient.watch(podList => { if (this._refreshing > 0) this._refreshing-- - if (podList.length === 0) return - - // podList should only contain 1 pod (due to name) - this._podWatchers[name].jolokiaPod = podList[0] + if (podList.length > 0) { + if (this._podWatchers[name]) { + // podList should only contain 1 pod (due to name) + this._podWatchers[name].jolokiaPod = podList[0] + } else { + log.warn(`[NamespaceClient ${this._namespace}]: pod watcher with name ${name} no longer exists yet still watching`) + } + } - if (this._refreshing === 0) { + if (this._refreshing <= 0) { // Limit callback to final watch returning - this._callback(this.getJolokiaPods(), this._podList.size) + this._callback(this.getJolokiaPods(), this._filteredList.length) } }) + log.debug(`[NamespaceClient ${this._namespace}]: connecting new pod client`) + /* * Pod is part of the current page so connect its pod watcher */ @@ -111,6 +182,16 @@ export class NamespaceClient implements Paging { jolokiaPod: undefined, } }) + + if (pagedPods.length === 0 || this._notifyChange) { + log.debug(`[NamespaceClient ${this._namespace}]: notifying since page is either empty or watcher configuration has changed`) + this._notifyChange = false + this._callback(this.getJolokiaPods(), this._filteredList.length) + } + } + + get namespace(): string { + return this._namespace } isConnected(): boolean { @@ -123,11 +204,21 @@ export class NamespaceClient implements Paging { this._limit = limit > 1 ? limit : 1 this._current = 0 + if (this.isFiltered()) { + /* + * Do not watch since this namespace has been filtered out + * Return no pods and zero total + */ + this._callback([], 0) + return + } + const _nsClient = clientFactory.create(this.initPodOptions(WatchTypes.PODS)) const _nsWatch = _nsClient.watch(pods => { /* - * Filter out any non-jolokia pods immediately and add - * the applicable pods to the pod name list + * Filter out any non-jolokia pods or + * any pods that do not conform to any pod filters + * immediately and add the applicable pods to the pod name list */ const podNames: string[] = [] pods @@ -187,6 +278,40 @@ export class NamespaceClient implements Paging { delete pr.jolokiaPod } this._podWatchers = {} + this._podList.clear() + } + + filter(typeFilter: TypeFilter) { + this._typeFilter = typeFilter + this._notifyChange = true + + /* + * If already connected then recreate the pod watchers + * according to the new type filter + */ + if (this.isConnected()) { + this.createPodWatchers() + } + else if (! this.isFiltered()) { + this.connect(this._limit) + } + + /* + * Not connected and does not conform to the + * namespace part of the type filter + */ + } + + sort(sortOrder: SortOrder) { + this._sortOrder = sortOrder + this._notifyChange = true + + /* + * If already connected then recreate the pod watchers + * according to the new sort order + */ + if (this.isConnected()) + this.createPodWatchers() } first() { diff --git a/packages/kubernetes-api/src/filter.ts b/packages/kubernetes-api/src/filter.ts new file mode 100644 index 00000000..1927143f --- /dev/null +++ b/packages/kubernetes-api/src/filter.ts @@ -0,0 +1,75 @@ +import { KubePod } from "./globals" + +export enum TypeFilterType { + NAME = 'Name', + NAMESPACE = 'Namespace', +} + +export function typeFilterTypeValueOf(str: string): TypeFilterType | undefined { + switch (str) { + case TypeFilterType.NAME: + return TypeFilterType.NAME + case TypeFilterType.NAMESPACE: + return TypeFilterType.NAMESPACE + default: + return undefined + } +} + +export class TypeFilter { + private _nsValues: Set + private _nameValues: Set + + constructor(nsValues?: string[], nameValues?: string[]) { + this._nsValues = new Set(nsValues ?? []) + this._nameValues = new Set(nameValues ?? []) + } + + get nsValues(): string[] { + return Array.from(this._nsValues) + } + + addNSValue(ns: string) { + this._nsValues.add(ns) + } + + deleteNSValue(ns: string) { + this._nsValues.delete(ns) + } + + get nameValues(): string[] { + return Array.from(this._nameValues) + } + + addNameValue(name: string) { + this._nameValues.add(name) + } + + deleteNameValue(name: string) { + this._nameValues.delete(name) + } + + filterNS(ns: string): boolean { + if (this._nsValues.size === 0) return true + + let resolved = false + this._nsValues.forEach(v => { + if (ns.includes(v)) + resolved = true + }) + + return resolved + } + + filterPod(podName: string): boolean { + if (this._nameValues.size === 0) return true + + let resolved = false + this._nameValues.forEach(v => { + if (podName.includes(v)) + resolved = true + }) + + return resolved + } +} diff --git a/packages/kubernetes-api/src/globals.ts b/packages/kubernetes-api/src/globals.ts index fe333c81..9d26657e 100644 --- a/packages/kubernetes-api/src/globals.ts +++ b/packages/kubernetes-api/src/globals.ts @@ -43,7 +43,7 @@ export type KubeProject = KubeObject & { } export type KubePodsOrError = { - total: number + fullPodCount: number pods: KubePod[] error?: Error } diff --git a/packages/kubernetes-api/src/index.ts b/packages/kubernetes-api/src/index.ts index a6d90ee4..d580896d 100644 --- a/packages/kubernetes-api/src/index.ts +++ b/packages/kubernetes-api/src/index.ts @@ -15,5 +15,7 @@ export async function isK8ApiRegistered(): Promise { } export * from './globals' +export * from './filter' +export * from './sort' export { k8Api, k8Service } from './init' export * from './utils' diff --git a/packages/kubernetes-api/src/kubernetes-service.ts b/packages/kubernetes-api/src/kubernetes-service.ts index e00023b3..a5e0b7a9 100644 --- a/packages/kubernetes-api/src/kubernetes-service.ts +++ b/packages/kubernetes-api/src/kubernetes-service.ts @@ -13,6 +13,8 @@ import { isError, pathGet } from './utils' import { clientFactory, log, Client, NamespaceClient } from './client' import { K8Actions, KubePod, KubePodsByProject, KubeProject, Paging } from './globals' import { k8Api } from './init' +import { TypeFilter } from './filter' +import { SortOrder } from './sort' export class KubernetesService extends EventEmitter implements Paging { private _loading = 0 @@ -25,6 +27,8 @@ export class KubernetesService extends EventEmitter implements Paging { private namespace_clients: { [namespace: string]: NamespaceClient } = {} private _nsLimit = 3 + private _typeFilter?: TypeFilter + private _sortOrder?: SortOrder async initialize(): Promise { if (this._initialized) return this._initialized @@ -55,11 +59,13 @@ export class KubernetesService extends EventEmitter implements Paging { } private initNamespaceClient(namespace: string) { - const cb = (jolokiaPods: KubePod[], jolokiaTotal: number, error?: Error) => { + const cb = (jolokiaPods: KubePod[], fullPodCount: number, error?: Error) => { + log.debug(`[KubeService ${namespace}]: callback: fullPodCount: ${fullPodCount}`, 'jolokia pods: ', jolokiaPods) + this._loading = this._loading > 0 ? this._loading-- : 0 if (isError(error)) { - this.podsByProject[namespace] = { total: jolokiaTotal, pods: [], error: error } + this.podsByProject[namespace] = { fullPodCount: fullPodCount, pods: [], error: error } this.emit(K8Actions.CHANGED) return } @@ -73,7 +79,12 @@ export class KubernetesService extends EventEmitter implements Paging { projectPods.push(jpod) } - this.podsByProject[namespace] = { total: jolokiaTotal, pods: projectPods } + if (projectPods.length === 0) + delete this.podsByProject[namespace] + else { + this.podsByProject[namespace] = { fullPodCount: fullPodCount, pods: projectPods } + } + this.emit(K8Actions.CHANGED) } @@ -285,12 +296,40 @@ export class KubernetesService extends EventEmitter implements Paging { return reason || 'unknown' } + /***************************** + * Filtering and Sort support + *****************************/ + sort(order: SortOrder) { + this._sortOrder = order + + Object.values(this.namespace_clients) + .forEach(client => { + client.sort(order) + }) + } + + filter(typeFilter: TypeFilter) { + this._typeFilter = new TypeFilter(typeFilter.nsValues, typeFilter.nameValues) + + Object.values(this.namespace_clients) + .forEach(client => { + client.filter(typeFilter) + }) + } + /******************** + * Paging interface + ********************/ first(namespace?: string) { if (!namespace) return - const namespaceClient = this.namespace_clients[namespace] - if (!namespaceClient) return + const nsClient = this.namespace_clients[namespace] + if (!nsClient) return + + if (! nsClient.isConnected) { + log.warn(`k8 Service cannot page on disconnected namespace ${namespace}`) + return + } this.namespace_clients[namespace].first() } @@ -298,8 +337,10 @@ export class KubernetesService extends EventEmitter implements Paging { hasPrevious(namespace?: string): boolean { if (!namespace) return false - const namespaceClient = this.namespace_clients[namespace] - if (!namespaceClient) return false + const nsClient = this.namespace_clients[namespace] + if (!nsClient) return false + + if (! nsClient.isConnected) return false return this.namespace_clients[namespace].hasPrevious() } @@ -307,8 +348,13 @@ export class KubernetesService extends EventEmitter implements Paging { previous(namespace?: string) { if (!namespace) return - const namespaceClient = this.namespace_clients[namespace] - if (!namespaceClient) return + const nsClient = this.namespace_clients[namespace] + if (!nsClient) return + + if (! nsClient.isConnected) { + log.warn(`k8 Service cannot page on disconnected namespace ${namespace}`) + return + } this.namespace_clients[namespace].previous() } @@ -316,8 +362,10 @@ export class KubernetesService extends EventEmitter implements Paging { hasNext(namespace?: string): boolean { if (!namespace) return false - const namespaceClient = this.namespace_clients[namespace] - if (!namespaceClient) return false + const nsClient = this.namespace_clients[namespace] + if (!nsClient) return false + + if (! nsClient.isConnected) return false return this.namespace_clients[namespace].hasNext() } @@ -325,8 +373,13 @@ export class KubernetesService extends EventEmitter implements Paging { next(namespace?: string) { if (!namespace) return - const namespaceClient = this.namespace_clients[namespace] - if (!namespaceClient) return + const nsClient = this.namespace_clients[namespace] + if (!nsClient) return + + if (! nsClient.isConnected) { + log.warn(`k8 Service cannot page on disconnected namespace ${namespace}`) + return + } this.namespace_clients[namespace].next() } @@ -334,8 +387,13 @@ export class KubernetesService extends EventEmitter implements Paging { last(namespace?: string) { if (!namespace) return - const namespaceClient = this.namespace_clients[namespace] - if (!namespaceClient) return + const nsClient = this.namespace_clients[namespace] + if (!nsClient) return + + if (! nsClient.isConnected) { + log.warn(`k8 Service cannot page on disconnected namespace ${namespace}`) + return + } this.namespace_clients[namespace].last() } @@ -343,8 +401,13 @@ export class KubernetesService extends EventEmitter implements Paging { page(pageIdx: number, namespace?: string) { if (!namespace) return - const namespaceClient = this.namespace_clients[namespace] - if (!namespaceClient) return + const nsClient = this.namespace_clients[namespace] + if (!nsClient) return + + if (! nsClient.isConnected) { + log.warn(`k8 Service cannot page on disconnected namespace ${namespace}`) + return + } this.namespace_clients[namespace].page(pageIdx) } diff --git a/packages/kubernetes-api/src/sort.ts b/packages/kubernetes-api/src/sort.ts new file mode 100644 index 00000000..21ac7058 --- /dev/null +++ b/packages/kubernetes-api/src/sort.ts @@ -0,0 +1,4 @@ +export enum SortOrder { + ASC = 'Ascending', + DESC = 'Descending' +} diff --git a/packages/management-api/src/globals.ts b/packages/management-api/src/globals.ts index f2ce3c47..91fac355 100644 --- a/packages/management-api/src/globals.ts +++ b/packages/management-api/src/globals.ts @@ -15,24 +15,3 @@ export enum MgmtActions { export type MPodsByUid = { [uid: string]: ManagedPod } export type ManagedProjects = { [key: string]: ManagedProject } - -export enum TypeFilterType { - NAME = 'Name', - NAMESPACE = 'Namespace', -} - -export function typeFilterTypeValueOf(str: string): TypeFilterType | undefined { - switch (str) { - case TypeFilterType.NAME: - return TypeFilterType.NAME - case TypeFilterType.NAMESPACE: - return TypeFilterType.NAMESPACE - default: - return undefined - } -} - -export type TypeFilter = { - type: TypeFilterType - values: Set -} diff --git a/packages/management-api/src/managed-pod.ts b/packages/management-api/src/managed-pod.ts index 00d58383..6f3a1306 100644 --- a/packages/management-api/src/managed-pod.ts +++ b/packages/management-api/src/managed-pod.ts @@ -5,7 +5,7 @@ import Jolokia, { } from 'jolokia.js' import 'jolokia.js/simple' import $ from 'jquery' -import { TypeFilter, log } from './globals' +import { log } from './globals' import jsonpath from 'jsonpath' import { k8Api, @@ -14,6 +14,7 @@ import { PodStatus, joinPaths, PodSpec, + TypeFilter, JOLOKIA_PORT_QUERY, } from '@hawtio/online-kubernetes-api' import { ParseResult, isJolokiaVersionResponseType, jolokiaResponseParse } from './jolokia-response-utils' @@ -259,23 +260,4 @@ export class ManagedPod { eventService.notify({ type: 'danger', message: msg }) } } - - filter(filter: TypeFilter): boolean { - const metadata = this.metadata - if (!metadata) return false - - type KubeObjKey = keyof typeof metadata - const podProp = metadata[filter.type.toLowerCase() as KubeObjKey] as string - - // Want to filter on this property but value - // is null so filter fails - if (!podProp) return false - - // values is tested as OR - for (const value of filter.values) { - if (podProp.toLowerCase().includes(value.toLowerCase())) return true - } - - return false - } } diff --git a/packages/management-api/src/managed-project.ts b/packages/management-api/src/managed-project.ts index bf897200..eba20458 100644 --- a/packages/management-api/src/managed-project.ts +++ b/packages/management-api/src/managed-project.ts @@ -5,7 +5,7 @@ import { ManagedPod } from './managed-pod' export class ManagedProject { private _error?: Error private podsByUid: MPodsByUid = {} - private _podTotal: number = 0 + private _fullPodCount: number = 0 constructor(private _name: string) {} @@ -13,12 +13,12 @@ export class ManagedProject { return this._name } - get podTotal(): number { - return this._podTotal + get fullPodCount(): number { + return this._fullPodCount } - set podTotal(podTotal: number) { - this._podTotal = podTotal + set fullPodCount(fullPodCount: number) { + this._fullPodCount = fullPodCount } get error(): Error | undefined { diff --git a/packages/management-api/src/management-service.ts b/packages/management-api/src/management-service.ts index 4a05da8f..5d4d1cea 100644 --- a/packages/management-api/src/management-service.ts +++ b/packages/management-api/src/management-service.ts @@ -10,6 +10,8 @@ import { debounce, KubePodsByProject, Paging, + SortOrder, + TypeFilter } from '@hawtio/online-kubernetes-api' import { ManagedProjects, MgmtActions, log } from './globals' import { ManagedProject } from './managed-project' @@ -32,6 +34,7 @@ export class ManagementService extends EventEmitter implements Paging { fireUpdate: false, uids: new Set(), } + private _orderingAndFiltering: boolean = false private _jolokiaPolling = 15000 private _pollingHandle?: NodeJS.Timeout @@ -55,6 +58,20 @@ export class ManagementService extends EventEmitter implements Paging { if (!this.hasError()) { const kPodsByProject: KubePodsByProject = k8Service.getPods() + /* + * Delete any projects no longer contained in the k8 service + */ + Object.keys(this._managedProjects) + .filter(ns => ! Object.keys(kPodsByProject).includes(ns)) + .forEach(ns => { + /* Flag this project to be removed in the update */ + this._managedProjects[ns].pods = [] + this._managedProjects[ns].fullPodCount = 0 + }) + + /* + * Update the remaining projects + */ Object.entries(kPodsByProject).forEach(([project, kPodsOrError]) => { /* * Either project has never been seen before so initialise @@ -68,10 +85,11 @@ export class ManagementService extends EventEmitter implements Paging { // Project may have an error mgmtProject.error = kPodsOrError - mgmtProject.podTotal = kPodsOrError.total mgmtProject.pods = kPodsOrError.pods + mgmtProject.fullPodCount = kPodsOrError.fullPodCount }) + // let's kick a polling cycle this.pollManagementData() } @@ -90,7 +108,7 @@ export class ManagementService extends EventEmitter implements Paging { private preMgmtUpdate() { /* Reset the update queue */ this.updateQueue.uids.clear() - this.updateQueue.fireUpdate = false + this.updateQueue.fireUpdate = this._orderingAndFiltering // Add all the uids to the queue Object.values(this._managedProjects) @@ -103,10 +121,17 @@ export class ManagementService extends EventEmitter implements Paging { this.updateQueue.uids.delete(emitter.uid) } - /* If the emitter should fire then update the queue */ - this.updateQueue.fireUpdate = emitter.fireUpdate ? emitter.fireUpdate : this.updateQueue.fireUpdate + /* + * If not already set to fire then check whether + * the emitter should fire then update the queue + */ + if (!this.updateQueue.fireUpdate) + this.updateQueue.fireUpdate = emitter.fireUpdate ? emitter.fireUpdate : this.updateQueue.fireUpdate - if (this.updateQueue.fireUpdate && this.updateQueue.uids.size === 0) this.emit(MgmtActions.UPDATED) + if (this.updateQueue.fireUpdate && this.updateQueue.uids.size === 0) { + this._orderingAndFiltering = false // reset ready for next time + this.emit(MgmtActions.UPDATED) + } } private async mgmtUpdate() { @@ -126,6 +151,7 @@ export class ManagementService extends EventEmitter implements Paging { const mPodsByUid = managedProject.pods if (Object.entries(mPodsByUid).length === 0) { + delete this._managedProjects[managedProject.name] this.emitUpdate({ fireUpdate: true }) continue } @@ -367,6 +393,19 @@ export class ManagementService extends EventEmitter implements Paging { connectService.connect(connection) } + /******************** + * Filtering & Sort support + ********************/ + filter(typeFilter: TypeFilter) { + this._orderingAndFiltering = true + k8Service.filter(typeFilter) + } + + sort(sortOrder: SortOrder) { + this._orderingAndFiltering = true + k8Service.sort(sortOrder) + } + /******************** * Paging interface ********************/ diff --git a/packages/online-shell/src/discover/Discover.tsx b/packages/online-shell/src/discover/Discover.tsx index be143ae9..49917915 100644 --- a/packages/online-shell/src/discover/Discover.tsx +++ b/packages/online-shell/src/discover/Discover.tsx @@ -3,26 +3,22 @@ import { Alert, Card, CardBody, - EmptyState, - EmptyStateBody, - EmptyStateIcon, PageSection, PageSectionVariants, Title, - EmptyStateHeader, Tabs, TabTitleText, Tab, } from '@patternfly/react-core' -import { CubesIcon } from '@patternfly/react-icons' import { discoverService } from './discover-service' import { DiscoverToolbar } from './DiscoverToolbar' import { DiscoverContext, useDisplayItems } from './context' import { DiscoverProjectContent } from './DiscoverProjectContent' import { DiscoverLoadingPanel } from './DiscoverLoadingPanel' +import { DiscoverEmptyContent } from './DiscoverEmptyContent' export const Discover: React.FunctionComponent = () => { - const { error, isLoading, refreshing, setRefreshing, discoverProjects, setDiscoverProjects, filters, setFilters } = + const { error, isLoading, refreshing, setRefreshing, discoverProjects, setDiscoverProjects, filter, setFilter } = useDisplayItems() const [activeTabKey, setActiveTabKey] = React.useState('') @@ -44,7 +40,7 @@ export const Discover: React.FunctionComponent = () => { return activeKey }) - }, [isLoading, error, discoverProjects, filters]) + }, [isLoading, error, discoverProjects, filter]) if (isLoading) { return ( @@ -78,23 +74,14 @@ export const Discover: React.FunctionComponent = () => { setRefreshing, discoverProjects, setDiscoverProjects, - filters, - setFilters, + filter, + setFilter, }} > {discoverProjects.length === 0 && ( - - } - headingLevel='h1' - /> - - There are no containers running with a port configured whose name is jolokia. - - + )} {discoverProjects.length > 0 && ( @@ -102,7 +89,7 @@ export const Discover: React.FunctionComponent = () => { {discoverProjects.map(discoverProject => ( {`${discoverProject.name} (${discoverProject.podsTotal})`}} + title={{`${discoverProject.name} (${discoverProject.fullPodCount})`}} key={`discover-project-${discoverProject.name}`} > diff --git a/packages/online-shell/src/discover/DiscoverEmptyContent.tsx b/packages/online-shell/src/discover/DiscoverEmptyContent.tsx new file mode 100644 index 00000000..0752ac06 --- /dev/null +++ b/packages/online-shell/src/discover/DiscoverEmptyContent.tsx @@ -0,0 +1,44 @@ +import React, { useContext } from 'react' +import { + EmptyState, + EmptyStateHeader, + EmptyStateIcon, + EmptyStateBody, + Panel, + PanelMain, + PanelMainBody, +} from '@patternfly/react-core' +import { CubesIcon } from '@patternfly/react-icons' +import { DiscoverContext } from './context' +import { DiscoverLoadingPanel } from './DiscoverLoadingPanel' + +export const DiscoverEmptyContent: React.FunctionComponent = () => { + const { refreshing } = useContext(DiscoverContext) + + return ( + + {refreshing && ( + + + + + + + + )} + + {! refreshing && ( + + } + headingLevel='h1' + /> + + There are no containers running with a port configured whose name is jolokia. + + + )} + + ) +} diff --git a/packages/online-shell/src/discover/DiscoverProjectContent.tsx b/packages/online-shell/src/discover/DiscoverProjectContent.tsx index 9c56ec7f..2c597b3a 100644 --- a/packages/online-shell/src/discover/DiscoverProjectContent.tsx +++ b/packages/online-shell/src/discover/DiscoverProjectContent.tsx @@ -1,6 +1,5 @@ -import React, { useContext } from 'react' +import React, { useContext, useEffect } from 'react' import { - Button, List, Pagination, Panel, @@ -12,13 +11,13 @@ import { ToolbarGroup, ToolbarItem, } from '@patternfly/react-core' -import { DiscoverProject } from './discover-project' +import { k8Service } from '@hawtio/online-kubernetes-api' +import { mgmtService } from '@hawtio/online-management-api' import { DiscoverGroupList } from './DiscoverGroupList' import { DiscoverPodItem } from './DiscoverPodItem' -import { mgmtService } from '@hawtio/online-management-api' import { DiscoverLoadingPanel } from './DiscoverLoadingPanel' import { DiscoverContext } from './context' -import { k8Service } from '@hawtio/online-kubernetes-api' +import { DiscoverProject } from './discover-project' interface DiscoverProjectCntProps { project: DiscoverProject @@ -30,6 +29,18 @@ export const DiscoverProjectContent: React.FunctionComponent { + if (refreshing) return + + if ((page * k8Service.namespaceLimit) > (props.project.fullPodCount + k8Service.namespaceLimit)) { + /* + * If filtering has caused the fullPodCount to decrease + * below the (page * limit) total then reset back to 1 + */ + setPage(1) + } + }, [refreshing]) + const firstPods = (page: number) => { setPage(page) setRefreshing(true) @@ -70,7 +81,7 @@ export const DiscoverProjectContent: React.FunctionComponent } -const descending: SortOrder = { id: 'descending', icon: } - -interface SortMetaType { - id: string - order: SortOrder +function sortTypeValueOf(str: string): SortType | undefined { + switch (str) { + case SortType.NAME: + return SortType.NAME + case SortType.NAMESPACE: + return SortType.NAMESPACE + default: + return undefined + } } -type FilterMeta = { - [name: string]: TypeFilterType +interface SortOrderIcon { + type: SortOrder + icon: ReactNode } -type SortMeta = { - [name: string]: SortMetaType -} +const ascending: SortOrderIcon = { type: SortOrder.ASC, icon: } +const descending: SortOrderIcon = { type: SortOrder.DESC, icon: } -const filterMeta: FilterMeta = { +const filterMeta = { name: TypeFilterType.NAME, namespace: TypeFilterType.NAMESPACE, } -const sortMeta: SortMeta = { +const sortMeta = { name: { - id: 'Name', - order: ascending, + id: SortType.NAME, + orderIcon: ascending, }, namespace: { - id: 'Namespace', - order: ascending, + id: SortType.NAMESPACE, + orderIcon: ascending, }, } export const DiscoverToolbar: React.FunctionComponent = () => { - const { discoverProjects, setDiscoverProjects, filters, setFilters } = useContext(DiscoverContext) + const { + discoverProjects, setDiscoverProjects, + filter, setFilter, setRefreshing } = useContext(DiscoverContext) // Ref for toggle of filter type Select control const filterTypeToggleRef = useRef() @@ -73,18 +79,23 @@ export const DiscoverToolbar: React.FunctionComponent = () => { const [filterInputPlaceholder, setFilterInputPlaceholder] = useState(defaultFilterInputPlaceholder) // Ref for toggle of sort type Select control - useRef() + const sortTypeToggleRef = useRef() // The type of sort to be created - chosen by the Select control - const [sortType, setSortType] = useState(sortMeta.name.id || '') + const [sortType, setSortType] = useState(sortMeta.name.id || '') // Flag to determine whether the Sort Select control is open or closed const [isSortTypeOpen, setIsSortTypeOpen] = useState(false) // Icon showing the sort - const [sortOrder, setSortOrder] = useState(sortMeta.name.order) + const [sortOrderIcon, setSortOrderIcon] = useState(sortMeta.name.orderIcon) + + const callMgmtFilter = (filter: TypeFilter) => { + setRefreshing(true) + setFilter(filter) + mgmtService.filter(filter) + } const clearFilters = () => { - const emptyFilters: TypeFilter[] = [] - setFilters(emptyFilters) setFilterInput('') + callMgmtFilter(new TypeFilter()) } const onSelectFilterType = (_event?: ChangeEvent | MouseEvent, value?: string | number) => { @@ -100,9 +111,11 @@ export const DiscoverToolbar: React.FunctionComponent = () => { const filterChips = (): string[] => { const chips: string[] = [] - filters.forEach(filter => { - Array.from(filter.values).map(v => chips.push(filter.type + ':' + v)) - }) + filter.nsValues + .map(v => chips.push(TypeFilterType.NAMESPACE + ':' + v)) + + filter.nameValues + .map(v => chips.push(TypeFilterType.NAME + ':' + v)) return chips } @@ -112,71 +125,91 @@ export const DiscoverToolbar: React.FunctionComponent = () => { if (!filterType) return - const newFilters = [...filters] - - const typeFilter = newFilters.filter(f => f.type === filterType) - if (typeFilter.length === 0) { - const filter: TypeFilter = { - type: filterType, - values: new Set([value]), - } - newFilters.push(filter) - } else { - typeFilter[0].values.add(value) + const newTypeFilter = new TypeFilter(filter.nsValues, filter.nameValues) + + switch (filterType) { + case TypeFilterType.NAME: + newTypeFilter.addNameValue(value) + break + case TypeFilterType.NAMESPACE: + newTypeFilter.addNSValue(value) } - setFilters(newFilters) + callMgmtFilter(newTypeFilter) } const deleteFilter = (filterChip: string) => { - const remaining = filters.filter(filter => { - const [typeId, value] = filterChip.split(':') - const type = typeFilterTypeValueOf(typeId) - - if (filter.type !== type) return true + const [typeId, value] = filterChip.split(':') + const filterType = typeFilterTypeValueOf(typeId) + const newTypeFilter = new TypeFilter(filter.nsValues, filter.nameValues) - filter.values.delete(value) - return filter.values.size > 0 - }) + switch (filterType) { + case TypeFilterType.NAME: + newTypeFilter.deleteNameValue(value) + break + case TypeFilterType.NAMESPACE: + newTypeFilter.deleteNSValue(value) + } - setFilters(remaining) + callMgmtFilter(newTypeFilter) } const onSelectSortType = (_event?: MouseEvent, value?: string | number) => { if (!value) return // Updates the sort type either Name or Namespace - setSortType(value as string) + const type = sortTypeValueOf(`${value}`) + setSortType(type as SortType) // Updates the sort order to whichever the sort type is set to - setSortOrder(value === sortMeta.name.id ? sortMeta.name.order : sortMeta.namespace.order) + setSortOrderIcon(value === sortMeta.name.id ? sortMeta.name.orderIcon : sortMeta.namespace.orderIcon) setIsSortTypeOpen(false) - filterTypeToggleRef?.current?.focus() + sortTypeToggleRef?.current?.focus() + } + + const isSortButtonEnabled = () => { + console.log(`SortType ${sortType}`) + switch (sortType) { + case SortType.NAMESPACE: + return Object.values(discoverProjects).length > 1 + case SortType.NAME: + return discoverProjects + .filter(discoverProject => (discoverProject.fullPodCount) > 1) + .length > 0 + default: + return true // enable by default + } } const sortItems = () => { - const sortedProjects = [...discoverProjects] - const newSortOrder = sortOrder === ascending ? descending : ascending + const newSortOrderIcon = sortOrderIcon === ascending ? descending : ascending + setSortOrderIcon(newSortOrderIcon) + + switch(sortType) { + case SortType.NAMESPACE: + const sortedProjects = [...discoverProjects] + /* + * Sorting via namespace requires simply moving around the + * tabs in the UI + */ + sortMeta.namespace.orderIcon = newSortOrderIcon.type === SortOrder.ASC ? ascending : descending - switch (sortType) { - case sortMeta.name.id: - // Sorting via name - sortMeta.name.order = newSortOrder - sortedProjects.forEach(project => { - project.sort(newSortOrder === ascending ? 1 : -1) - }) - break - case sortMeta.namespace.id: - // Sorting via namespace - sortMeta.namespace.order = newSortOrder sortedProjects.sort((ns1: DiscoverProject, ns2: DiscoverProject) => { let value = ns1.name.localeCompare(ns2.name) - return newSortOrder === descending ? (value *= -1) : value + return newSortOrderIcon === descending ? (value *= -1) : value }) - } - setDiscoverProjects(sortedProjects) - setSortOrder(newSortOrder) + setDiscoverProjects(sortedProjects) + break + case SortType.NAME: + /* + * Resorting the pod names required going back to k8 level + * in order to correctly sort all pods and get back the right + * set according to the paging limit + */ + setRefreshing(true) + mgmtService.sort(newSortOrderIcon.type) + } } return ( @@ -225,15 +258,14 @@ export const DiscoverToolbar: React.FunctionComponent = () => { /> +