@spotinst/spinnaker-deck
Version:
Spinnaker-Deck service, forked with support to Spotinst
294 lines (253 loc) • 10.5 kB
text/typescript
import { IScope } from 'angular';
import { map, union, uniq } from 'lodash';
import { $log, $q } from 'ngimport';
import { Observable, ReplaySubject, Subject, Subscription } from 'rxjs';
import { ICluster } from '../domain/ICluster';
import { ApplicationDataSource, IDataSourceConfig, IFetchStatus } from './service/applicationDataSource';
export class Application {
[k: string]: any;
/**
* A collection of all available data sources for the given application
* @type {Array}
*/
public dataSources: ApplicationDataSource[] = [];
/**
* The name of the application
*/
public get name(): string {
return this.applicationName;
}
/**
* A list of all accounts currently used within the application. Accounts come from either:
* - those explicitly configured on the application (to be removed soon), or
* - a dataSource configured with a credentialsField
* @type {Array}
*/
public accounts: string[] = [];
/**
* A list of all cluster currently used within the application.
* @type {Array}
*/
public clusters: ICluster[] = [];
/**
* A timestamp indicating the last time the onRefresh method succeeded
*/
public lastRefresh: number;
/**
* A map where the key is the provider and the value is the default credentials value.
* Default values are determined by querying all data sources with a credentialsField.
* IFF only one unique value is found, that value is set in the map. Otherwise, the provider is not present in the map
* @type {Map<string, string>}
*/
public defaultCredentials: any = {} as any;
/**
* A map where the key is the provider and the value is the default region value.
* Default values are determined by querying all data sources with a regionField.
* IFF only one unique value is found, that value is set in the map. Otherwise, the provider is not present in the map
* @type {Map<string, string>}
*/
public defaultRegions: any = {} as any;
/**
* An arbitrary collection of attributes coming from Front50
* @type {Map<string, string>}
*/
public attributes: any = {} as any;
/**
* Indicates that the application was not found in Front50
* @type {boolean}
*/
public notFound = false;
/**
* Indicates that there was an exception while trying to load or create
* the application model.
* @type {boolean}
*/
public hasError = false;
/**
* Indicates that the application does not exist and is used as a stub
* @type {boolean}
*/
public isStandalone = false;
/**
* If managed delivery is enabled, indicates whether an entire application is in an explicit paused state
*/
public isManagementPaused = false;
/**
* Which data source is the active state
* @type {ApplicationDataSource}
*/
public activeState: ApplicationDataSource = null;
private refreshListeners: Array<() => void> = [];
public activeDataSource$ = new ReplaySubject<ApplicationDataSource>(1);
/** @deprecated use activeDataSource$ */
public activeStateChangeStream: Subject<any> = new Subject();
private refreshStream: Subject<any> = new Subject();
private refreshFailureStream: Subject<any> = new Subject();
// Stream of fetch status
public status$: Observable<IFetchStatus>;
private dataLoader: Subscription;
constructor(
private applicationName: string,
private scheduler: any,
dataSourceConfigs: Array<IDataSourceConfig<any>>,
) {
dataSourceConfigs.forEach((config) => this.addDataSource(config));
this.status$ = Observable.combineLatest(this.dataSources.map((ds) => ds.status$)).map((statuses) =>
this.getDerivedApplicationStatus(statuses),
);
}
private getDerivedApplicationStatus(statuses: IFetchStatus[]): IFetchStatus {
const ERROR_STATUS = statuses.some(({ status }) => status === 'ERROR') ? 'ERROR' : undefined;
const FETCHING_STATUS = statuses.some(({ status }) => status === 'FETCHING') ? 'FETCHING' : undefined;
const FETCHED_STATUS = statuses.some(({ status }) => status === 'FETCHED') ? 'FETCHED' : undefined;
const rolledUpStatus: IFetchStatus['status'] =
ERROR_STATUS || FETCHING_STATUS || FETCHED_STATUS || 'NOT_INITIALIZED';
const noneLeftToFetch = statuses.every(({ status }) => ['FETCHED', 'NOT_INITIALIZED'].includes(status));
const lastFetch = statuses.reduce((latest, status) => Math.max(latest, status.lastRefresh || 0), 0);
const allLoaded = statuses.every(({ loaded }) => loaded);
return {
status: rolledUpStatus,
lastRefresh: noneLeftToFetch ? lastFetch : this.lastRefresh,
loaded: allLoaded,
data: undefined,
};
}
private addDataSource<T>(config: IDataSourceConfig<T>) {
const dataSource = new ApplicationDataSource(config, this);
this.dataSources.push(dataSource);
this[config.key] = dataSource;
}
/**
* Returns a data source based on its key. Data sources can be accessed on the application directly via the key,
* e.g. application.serverGroups, but this is the preferred access method, as it allows type inference
* @param key
*/
public getDataSource(key: string): ApplicationDataSource {
return this.dataSources.find((ds) => ds.key === key);
}
/**
* Refreshes all dataSources for the application
* @param forceRefresh if true, will trigger a refresh on all data sources, even if the data source is currently
* loading
* @returns {PromiseLike<void>} a promise that resolves when the application finishes loading, rejecting with an error if
* one of the data sources fails to refresh
*/
public refresh(forceRefresh?: boolean): PromiseLike<any> {
// refresh hidden data sources but do not consider their results when determining when the refresh completes
this.dataSources.filter((ds) => !ds.visible).forEach((ds) => ds.refresh(forceRefresh));
this.refreshListeners.forEach((cb) => cb());
return $q.all(this.dataSources.filter((ds) => ds.visible).map((source) => source.refresh(forceRefresh))).then(
() => this.applicationLoadSuccess(),
(error) => this.applicationLoadError(error),
);
}
/**
* A promise that resolves immediately if all data sources are ready (i.e. loaded), or once all data sources have
* loaded
* @returns {PromiseLike<any>} the return value is a promise, but its value is
* not useful - it's only useful to watch the promise itself
*/
public ready(): PromiseLike<any> {
return $q.all(
this.dataSources.filter((ds) => ds.onLoad !== undefined && ds.visible).map((dataSource) => dataSource.ready()),
);
}
/**
* Used to subscribe to the application's refresh cycle. Will automatically be disposed when the $scope is destroyed.
* @param $scope the $scope that will manage the lifecycle of the subscription
* If you pass in null for the $scope, you are responsible for unsubscribing when your component unmounts.
* @param method the method to call when the refresh completes
* @param failureMethod a method to call if the refresh fails
* @return a method to call to unsubscribe
*/
public onRefresh($scope: IScope, method: any, failureMethod?: any): () => void {
const success: Subscription = this.refreshStream.subscribe(method);
let failure: Subscription = null;
if (failureMethod) {
failure = this.refreshFailureStream.subscribe(failureMethod);
}
const unsubscribe = () => {
success.unsubscribe();
if (failure) {
failure.unsubscribe();
}
};
if ($scope) {
$scope.$on('$destroy', () => unsubscribe());
}
return unsubscribe;
}
/**
* This is really only used by the ApplicationController - it manages the refresh cycle for the overall application
* and halts refresh when switching applications or navigating to a non-application view
*/
public enableAutoRefresh(): void {
this.dataLoader = this.scheduler.subscribe(() => this.refresh());
}
public disableAutoRefresh(): void {
this.dataLoader && this.dataLoader.unsubscribe();
this.scheduler.unsubscribe();
}
public setActiveState(dataSource: ApplicationDataSource = null): void {
if (this.activeState !== dataSource) {
this.activeState = dataSource;
this.activeDataSource$.next(dataSource);
this.activeStateChangeStream.next(null);
}
}
private applicationLoadError(err: Error): void {
$log.error(err, 'Failed to load application, will retry on next scheduler execution.');
this.refreshFailureStream.next(err);
}
private applicationLoadSuccess(): void {
this.setApplicationAccounts();
this.setDefaults();
this.lastRefresh = new Date().getTime();
this.refreshStream.next(null);
}
private setApplicationAccounts(): void {
let accounts = this.accounts.concat(this.attributes.accounts || []);
this.dataSources
.filter((ds) => Array.isArray(ds.data) && ds.credentialsField !== undefined)
.forEach(
(ds: ApplicationDataSource<any[]>) => (accounts = accounts.concat(ds.data.map((d) => d[ds.credentialsField]))),
);
this.accounts = uniq(accounts);
}
private extractProviderDefault(field: string): Map<string, string> {
const results = new Map<string, string>();
const sources: Array<ApplicationDataSource<any[]>> = this.dataSources.filter(
(ds) => ds[field] !== undefined && Array.isArray(ds.data) && ds.providerField !== undefined,
);
const providers: string[][] = sources
.map((ds) => ds.data.map((d) => d[ds.providerField]))
.filter((p) => p.length > 0);
const allProviders = union(...providers);
allProviders.forEach((provider: string) => {
const vals = sources
.map((ds) =>
map(
ds.data.filter((d: any) => typeof d === 'object' && d[ds.providerField] === provider),
ds[field],
),
)
.filter((v) => v.length > 0);
const allValues = union(...vals);
if (allValues.length === 1) {
(results as any)[provider] = allValues[0];
}
});
return results;
}
private setDefaults(): void {
this.defaultCredentials = this.extractProviderDefault('credentialsField');
this.defaultRegions = this.extractProviderDefault('regionField');
}
public subscribeToRefresh(onRefreshCb: () => void) {
this.refreshListeners.push(onRefreshCb);
const unsubscribeCb = () => {
this.refreshListeners.filter((cb) => cb !== onRefreshCb);
};
return unsubscribeCb;
}
}