import { clearTimeout , setTimeout } from 'timers' ; import * as vscode from 'vscode' ; import { Host } from '../../host' ; import { Kubectl } from '../../kubectl' ; import * as kubectlUtils from '../../kubectlUtils' ; import { sleep } from '../../sleep' ; import * as providerResult from '../../utils/providerresult' ; import { refreshExplorer } from '../clusterprovider/common/explorer' ; import { affectsUs , getResourcesToBeWatched } from '../config/config' ; import { WatchManager } from '../kubectl/watch' ; import { ExplorerExtender , ExplorerUICustomizer } from './explorer.extension' ; import { ClusterExplorerNode , ClusterExplorerResourceNode } from './node' ; import { ContextNode , MiniKubeContextNode } from './node.context' ; export const KUBERNETES_EXPLORER_NODE_CATEGORY = 'kubernetes-explorer-node' ; export type KubernetesExplorerNodeTypeError = 'error' ; export type KubernetesExplorerNodeTypeContext = 'context' ; export type KubernetesExplorerNodeTypeResourceFolder = 'folder.resource' ; export type KubernetesExplorerNodeTypeGroupingFolder = 'folder.grouping' ; export type KubernetesExplorerNodeTypeResource = 'resource' ; export type KubernetesExplorerNodeTypeConfigItem = 'configitem' ; export type KubernetesExplorerNodeTypeHelmRelease = 'helm.release' ; export type KubernetesExplorerNodeTypeHelmHistory = 'helm.history' ; export type KubernetesExplorerNodeTypeExtension = 'extension' ; export type KubernetesExplorerNodeType = KubernetesExplorerNodeTypeError | KubernetesExplorerNodeTypeContext | KubernetesExplorerNodeTypeResourceFolder | KubernetesExplorerNodeTypeGroupingFolder | KubernetesExplorerNodeTypeResource | KubernetesExplorerNodeTypeConfigItem | KubernetesExplorerNodeTypeHelmRelease | KubernetesExplorerNodeTypeHelmHistory | KubernetesExplorerNodeTypeExtension ; export const NODE_TYPES = { error : 'error' , context : 'context' , folder : { resource : 'folder.resource' , grouping : 'folder.grouping' , } , resource : 'resource' , configitem : 'configitem' , helm : { release : 'helm.release' , history : 'helm.history' , } , extension : 'extension' } as const ; export function create ( kubectl , host ) { return new KubernetesExplorer ( kubectl , host ) ; } export function isKubernetesExplorerResourceNode ( obj ) { return obj && obj . nodeCategory === KUBERNETES_EXPLORER_NODE_CATEGORY && obj . nodeType === 'resource' ; } export class KubernetesExplorer implements vscode . TreeDataProvider < ClusterExplorerNode > { private onDidChangeTreeDataEmitter : vscode . EventEmitter < ClusterExplorerNode | undefined > = new vscode . EventEmitter < ClusterExplorerNode | undefined > ( ) ; readonly onDidChangeTreeData : vscode . Event < ClusterExplorerNode | undefined > = this . onDidChangeTreeDataEmitter . event ; private readonly extenders = Array . of < ExplorerExtender < ClusterExplorerNode > > ( ) ; private readonly customisers = Array . of < ExplorerUICustomizer < ClusterExplorerNode > > ( ) ; private refreshTimer : NodeJS . Timer ; private readonly refreshQueue = Array < ClusterExplorerNode > ( ) ; constructor ( private readonly kubectl , private readonly host ) { host . onDidChangeConfiguration ( ( change ) => { if ( affectsUs ( change ) ) { this . refresh ( ) ; } } ) ; } initialize ( ) { const viewer = vscode . window . createTreeView ( 'extension.vsKubernetesExplorer' , { treeDataProvider : this , showCollapseAll : true } ) ; return vscode . Disposable . from ( viewer , viewer . onDidCollapseElement ( this . onElementCollapsed , this ) , viewer . onDidExpandElement ( this . onElementExpanded , this ) ) ; } getTreeItem ( element ) { const baseTreeItem = element . getTreeItem ( ) ; const extensionAwareTreeItem = providerResult . transform ( baseTreeItem , ( ti ) => { if ( 'kind' in element && 'apiName' in element . kind && element . kind . apiName ) { ti . contextValue += 'k8s-watchable' ; } if ( ti . collapsibleState === vscode . TreeItemCollapsibleState . None && this . extenders . some ( ( e ) => e . contributesChildren ( element ) ) ) { ti . collapsibleState = vscode . TreeItemCollapsibleState . Collapsed ; } } ) ; let customisedTreeItem = extensionAwareTreeItem ; for ( const c of this . customisers ) { customisedTreeItem = providerResult . transformPossiblyAsync ( extensionAwareTreeItem , ( ti ) => c . customize ( element , ti ) ) ; } return customisedTreeItem ; } getChildren ( parent ? ) { const baseChildren = this . getChildrenBase ( parent ) ; const contributedChildren = this . extenders . filter ( ( e ) => e . contributesChildren ( parent ) ) . map ( ( e ) => e . getChildren ( parent ) ) ; return providerResult . append ( baseChildren , ... contributedChildren ) ; } async watch ( node ) { const id = this . getIdForWatch ( node ) ; if ( ! id ) { console . log ( 'Failed getting id for watch.' ) ; return ; } const namespace = await kubectlUtils . currentNamespace ( this . kubectl ) ; const apiUri = await node . apiURI ( this . kubectl , namespace ) ; if ( ! apiUri ) { console . log ( 'Api URI is not valid.' ) ; return ; } const onWatchNotification = ( type , _obj ) => { if ( type ) { this . queueRefresh ( node ) ; } } ; WatchManager . instance ( ) . addWatch ( id , apiUri , undefined , onWatchNotification ) ; } stopWatching ( node ) { const id = this . getIdForWatch ( node ) ; if ( id ) { WatchManager . instance ( ) . removeWatch ( id ) ; } } private getChildrenBase ( parent ? ) { if ( parent ) { return parent . getChildren ( this . kubectl , this . host ) ; } return this . getClusters ( ) ; } refresh ( node ? ) { this . onDidChangeTreeDataEmitter . fire ( node ) ; } registerExtender ( extender ) { this . extenders . push ( extender ) ; if ( extender . contributesChildren ( undefined ) ) { this . queueRefresh ( ) ; } } registerUICustomiser ( customiser ) { this . customisers . push ( customiser ) ; this . queueRefresh ( ) ; } queueRefresh ( node ? ) { if ( ! node ) { sleep ( 50 ) . then ( ( ) => refreshExplorer ( ) ) ; } else { const currentNodeId = this . getIdForWatch ( node ) ; if ( ! currentNodeId ) { return ; } clearTimeout ( this . refreshTimer ) ; const alreadyInQueue = this . refreshQueue . some ( ( queueEntry ) => currentNodeId === this . getIdForWatch ( queueEntry ) ) ; if ( ! alreadyInQueue ) { this . refreshQueue . push ( node ) ; } this . refreshTimer = setTimeout ( ( ) => { this . refreshQueue . splice ( 0 ) . forEach ( ( n ) => this . refresh ( n ) ) ; } , 500 ) ; } } private onElementCollapsed ( e ) { this . collapse ( e . element ) ; } private onElementExpanded ( e ) { this . expand ( e . element ) ; } private expand ( node ) { const watchId = this . getIdForWatch ( node ) ; const treeItem = node . getTreeItem ( ) ; providerResult . transform ( treeItem , ( ti ) => { if ( this . shouldCreateWatch ( ti . label , watchId ) ) { this . watch ( node ) ; } } ) ; } private collapse ( node ) { const watchId = this . getIdForWatch ( node ) ; if ( watchId && WatchManager . instance ( ) . existsWatch ( watchId ) ) { WatchManager . instance ( ) . removeWatch ( watchId ) ; } } private shouldCreateWatch ( label , watchId ) { if ( ! watchId ) { return false ; } if ( WatchManager . instance ( ) . existsWatch ( watchId ) ) { return true ; } const resourcesToWatch = getResourcesToBeWatched ( ) ; if ( label && resourcesToWatch . length > 0 && resourcesToWatch . indexOf ( treeItemLabel ( label ) ) !== - 1 ) { return true ; } return false ; } public getIdForWatch ( node ) { switch ( node . nodeType ) { case 'folder.resource' : { return node . kind . abbreviation ; } case 'resource' : { return node . kindName ; } default : { break ; } } return undefined ; } private async getClusters ( ) { const contexts = await kubectlUtils . getContexts ( this . kubectl , { silent : false } ) ; return contexts . map ( ( context ) => { if ( context . contextName === 'minikube' ) { return new MiniKubeContextNode ( context . contextName , context ) ; } return new ContextNode ( context . contextName , context ) ; } ) ; } } function treeItemLabel ( label ) { if ( isTreeItemLabel ( label ) ) { return label . label ; } return label ; } function isTreeItemLabel ( label ) { return ( label as any ) . label !== undefined ; }