Skip to content
This repository has been archived by the owner on May 7, 2021. It is now read-only.

Commit

Permalink
Merge pull request #469 from jstrachan/malarkey
Browse files Browse the repository at this point in the history
fixes #456 to make the environments page lazy load
  • Loading branch information
jstrachan authored May 12, 2017
2 parents 13245a1 + 9123ef5 commit 1ab1d5d
Show file tree
Hide file tree
Showing 27 changed files with 257 additions and 181 deletions.
91 changes: 71 additions & 20 deletions src/app/kubernetes/support/abstract-watch.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {OnDestroy} from "@angular/core";
import {Observable} from "rxjs";
import {Observable, Subject, Subscriber, Subscription} from "rxjs";
import {NamespacedResourceService} from "../service/namespaced.resource.service";
import {KubernetesResource} from "../model/kubernetesresource.model";
import {enrichServiceWithRoute, Service, Services} from "../model/service.model";
Expand All @@ -24,18 +24,24 @@ import {ReplicaSetService} from "../service/replicaset.service";
* component has been used
*/
export class AbstractWatchComponent implements OnDestroy {
private listObservableCache: Map<string, Observable<any[]>> = new Map<string, Observable<any[]>>();
public subjectCache: Map<string, Subject<any[]>> = new Map<string, Subject<any[]>>();
private watchCache: Map<string, Watcher> = new Map<string, Watcher>();

ngOnDestroy(): void {
for (let key in this.subjectCache) {
let subject = this.subjectCache[key];
if (subject) {
subject.unsubscribe();
}
}
this.subjectCache.clear();
for (let key in this.watchCache) {
let watch = this.watchCache[key];
if (watch) {
watch.close();
}
}
this.watchCache.clear();
this.listObservableCache.clear();
}

protected listAndWatchServices(namespace: string, serviceService: ServiceService, routeService: RouteService): Observable<Services> {
Expand All @@ -48,28 +54,32 @@ export class AbstractWatchComponent implements OnDestroy {


listAndWatchDeployments(namespace: string, deploymentService: DeploymentService, deploymentConfigService: DeploymentConfigService, serviceService: ServiceService, routeService: RouteService): Observable<DeploymentViews> {
const servicesObservable = this.listAndWatchServices(namespace, serviceService, routeService);

let deployments = Observable.combineLatest(
this.listAndWatch(deploymentService, namespace, Deployment),
this.listAndWatch(deploymentConfigService, namespace, DeploymentConfig),
combineDeployments,
);
let runtimeDeployments = Observable.combineLatest(
deployments,
this.listAndWatchServices(namespace, serviceService, routeService),
servicesObservable,
createDeploymentViews,
);
return runtimeDeployments;
}

listAndWatchReplicas(namespace: string, replicaSetService: ReplicaSetService, replicationControllerService: ReplicationControllerService, serviceService: ServiceService, routeService: RouteService): Observable<ReplicaSetViews> {
const servicesObservable = this.listAndWatchServices(namespace, serviceService, routeService);

let replicas = Observable.combineLatest(
this.listAndWatch(replicaSetService, namespace, ReplicaSet),
this.listAndWatch(replicationControllerService, namespace, ReplicationController),
combineReplicaSets,
);
let replicaViews = Observable.combineLatest(
replicas,
this.listAndWatchServices(namespace, serviceService, routeService),
servicesObservable,
createReplicaSetViews,
);
return replicaViews;
Expand All @@ -80,29 +90,34 @@ export class AbstractWatchComponent implements OnDestroy {
service: NamespacedResourceService<T, L>,
namespace: string,
type: { new (): T; }
) {
return Observable.combineLatest(
this.getOrCreateList(service, namespace, type),
// We just emit an empty item if the watch fails
this.getOrCreateWatch(service, namespace, type).dataStream.catch(() => Observable.of(null)),
(list, msg) => this.combineListAndWatchEvent(list, msg, service, type, namespace),
): Observable<L> {
let key = namespace + "/" + type.name;
return this.getOrCreateSubject(key, () =>
Observable.combineLatest(
//this.getOrCreateList(service, namespace, type),
service.list(namespace),
// We just emit an empty item if the watch fails
this.getOrCreateWatch(service, namespace, type)
.dataStream.catch(() => Observable.of(null)),
(list, msg) => this.combineListAndWatchEvent(list, msg, service, type, namespace),
)
);
}

protected getOrCreateList<T extends KubernetesResource, L extends Array<T>>(
service: NamespacedResourceService<T, L>,
namespace: string,
type: { new (): T; }
protected getOrCreateSubject<T extends KubernetesResource, L extends Array<T>>(
key: string,
createObserverFn: () => Observable<L>
): Observable<L> {
let key = namespace + "/" + type.name;
let answer = this.listObservableCache[key];
let answer = this.subjectCache[key];
if (!answer) {
answer = service.list(namespace);
this.listObservableCache[key] = answer;
let observable = createObserverFn();
answer = new CachingSubject(observable);
this.subjectCache[key] = answer;
}
return answer;
return answer.asObservable();
}


protected getOrCreateWatch<T extends KubernetesResource, L extends Array<T>>(
service: NamespacedResourceService<T, L>,
namespace: string,
Expand Down Expand Up @@ -192,6 +207,42 @@ export class AbstractWatchComponent implements OnDestroy {
}
}

/**
* Lets send the last value to any new subscriber before any new values.
*
* Unlike BehaviorSubject there is no need for an initial value.
* Unlike AsyncSubject we don't need to wait for complete before sending a next event
*/
export class CachingSubject<T> extends Subject<T> {
private _value: T;
private _hasValue = false;
private _subscription: Subscription;

constructor(protected observable: Observable<T>) {
super();
this._subscription = observable.subscribe(this);
}

unsubscribe(): void {
this._subscription.unsubscribe();
super.unsubscribe();
}

next(value?: T): void {
this._value = value;
this._hasValue = true;
super.next(value);
}


protected _subscribe(subscriber: Subscriber<T>): Subscription {
if (this._hasValue) {
subscriber.next(this._value);
}
return super._subscribe(subscriber);
}
}

/**
* Lets create a new array instance to force an update event on insert or delete to lists
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<div class='builds list'>
<fabric8-builds-list-toolbar></fabric8-builds-list-toolbar>
<fabric8-builds-list [builds]="builds | async" [loading]="loading | async"></fabric8-builds-list>
<div class="list-group list-view-pf list-view-pf-view">
<fabric8-builds-list [builds]="builds | async" [loading]="loading | async"></fabric8-builds-list>
</div>
</div>
2 changes: 1 addition & 1 deletion src/app/kubernetes/ui/build/list/list.build.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class='list-group list-view-pf list-view-pf-view'>
<div class='kube-resource-list'>

<fabric8-loading [loading]="loading">
<div class='list-group-item build' *ngFor='let build of builds'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<div class='buildconfigs list'>
<fabric8-buildconfigs-list-toolbar></fabric8-buildconfigs-list-toolbar>
<fabric8-buildconfigs-list [buildconfigs]="buildconfigs | async" [loading]="loading | async"></fabric8-buildconfigs-list>
<div class="list-group list-view-pf list-view-pf-view">
<fabric8-buildconfigs-list [buildconfigs]="buildconfigs | async"
[loading]="loading | async"></fabric8-buildconfigs-list>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class='list-group list-view-pf list-view-pf-view'>
<div class='kube-resource-list'>

<fabric8-loading [loading]="loading">
<div class='list-group-item buildconfig' *ngFor='let buildconfig of buildconfigs'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<div class='configmaps list'>
<fabric8-configmaps-list-toolbar></fabric8-configmaps-list-toolbar>
<fabric8-configmaps-list [configmaps]="configmaps | async" [loading]="loading | async"></fabric8-configmaps-list>
<div class="list-group list-view-pf list-view-pf-view">
<fabric8-configmaps-list [configmaps]="configmaps | async" [loading]="loading | async"></fabric8-configmaps-list>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class='list-group list-view-pf list-view-pf-view'>
<div class='kube-resource-list'>

<fabric8-loading [loading]="loading">
<div class='list-group-item configmap' *ngFor='let configmap of configmaps'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<div class='deployments list'>
<fabric8-deployments-list-toolbar></fabric8-deployments-list-toolbar>
<fabric8-deployments-list [runtimeDeployments]="runtimeDeployments | async" [loading]="loading | async"></fabric8-deployments-list>
<div class="list-group list-view-pf list-view-pf-view">
<fabric8-deployments-list [runtimeDeployments]="runtimeDeployments | async" [loading]="loading | async"></fabric8-deployments-list>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class='list-group list-view-pf list-view-pf-view'>
<div class='kube-resource-list'>

<fabric8-loading [loading]="loading">
<div class='list-group-item deployment' *ngFor='let deployment of runtimeDeployments'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,39 @@ export class Kind {
}

export class KindNode {
title: Subject<string>;
environment: Environment;
kind: Kind;
children: [
{
loading: Observable<boolean>,
data: ConnectableObservable<any[]>,
children: LazyLoadingData[];

constructor(public kind: Kind, public environment: Environment, public title: Observable<string>, protected loader: (KindNode) => LoadingData) {
this.children = [new LazyLoadingData(() => this.loader(this))];
}
}

export class LoadingData {
constructor(public loading: Observable<boolean>, public data: Observable<any[]>) {
}
}

export class LazyLoadingData {
private _loadingData: LoadingData;

constructor(protected loader: () => LoadingData) {
}

get loading(): Observable<boolean> {
return this.loadingData.loading;
}

get data(): Observable<any[]> {
return this.loadingData.data;
}

protected get loadingData(): LoadingData {
if (!this._loadingData) {
this._loadingData = this.loader();
//this._loadingData.data.connect();
}
];
return this._loadingData;
}
}

@Component({
Expand Down Expand Up @@ -134,54 +158,38 @@ export class EnvironmentListPageComponent extends AbstractWatchComponent impleme
environment: environment,
openshiftConsoleUrl: environmentOpenShiftConoleUrl(environment),
kinds: KINDS.map(kind => {
// Give it a default title

let title = new BehaviorSubject(`${kind.name}s`);
let loading = new BehaviorSubject(true);
let data = this.getCachedList(kind.path, environment)
// Update the title with the number of objects
.distinctUntilChanged()
.map(arr => {
if (label) {
return arr.filter(val => {
// lets only filter resources with a space label
return !val.labels['space'] || val.labels['space'] === label;
});
} else {

let loader = () => {
console.log(`Now loading data for ${kind.name} and environment ${environment.name}`);

let loading = new BehaviorSubject(true);
let data = this.getList(kind.path, environment)
.distinctUntilChanged()
.map(arr => {
if (label) {
arr = arr.filter(val => {
// lets only filter resources with a space label
return !val.labels['space'] || val.labels['space'] === label;
});
}
loading.next(false);

// TODO this seems to lock up the entire browser! :)
//title.next(`${arr.length} ${kind.name}${arr.length === 1 ? '' : 's'}`);
return arr;
}
})
.do(arr => title.next(`${arr.length} ${kind.name}${arr.length === 1 ? '' : 's'}`))
.do(() => loading.next(false))
.publishReplay(1);
return {
environment: environment,
kind: kind,
title: title,
children: [
{
loading: loading,
data: data,
},
],
} as KindNode;
});
return new LoadingData(loading, data);
};
return new KindNode(kind, environment, title.asObservable(), loader);
}),
})),
))
// Wait 200ms before publishing an empty value - it's probably not empty but it might be!
.debounce(arr => (arr.length > 0 ? Observable.interval(0) : Observable.interval(200)))
//.debounce(arr => (arr.length > 0 ? Observable.interval(0) : Observable.interval(200)))
.do(() => this.loading.next(false))
.publish();
// Now, connect all the data
// Note we don't do this inside main stream to allow the page to draw faster
this.environments.subscribe(
envs => envs.forEach(
env => env.kinds.forEach(
kind => kind.children.forEach(
child => child.data.connect(),
),
),
),
);
this.environments.connect();
this.space.connect();
}
Expand All @@ -194,21 +202,6 @@ export class EnvironmentListPageComponent extends AbstractWatchComponent impleme
// TODO is there a way to disconnect from this.space / this.environments?
}


/**
* Lets cache the observables so that we don't requery the services each time we ask for the observables
*/
private getCachedList(kind: string, environment: Environment): Observable<any[]> {
let namespace = environment.namespace.name;
let key = (namespace || "") + "/" + kind;
var answer = this.listCache[key];
if (!answer) {
answer = this.getList(kind, environment);
this.listCache[key] = answer;
}
return answer;
}

private getList(kind: string, environment: Environment): Observable<any[]> {
let namespace = environment.namespace.name;
switch (kind) {
Expand All @@ -230,6 +223,7 @@ export class EnvironmentListPageComponent extends AbstractWatchComponent impleme
}
}


export function environmentOpenShiftConoleUrl(environment: Environment): string {
let openshiftConsoleUrl = process.env.OPENSHIFT_CONSOLE_URL;
let namespace = environment.namespaceName;
Expand Down
Loading

0 comments on commit 1ab1d5d

Please sign in to comment.