Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Workloads Table #263

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions custom-src/frontend/app/custom/kubernetes/store/kube.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,9 +450,9 @@ export interface Volume {
}


export interface ConfigMap {
export interface ConfigMap<T = Item> {
name: string;
items: Item[];
items: T[];
defaultMode: number;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export class GetKubernetesPodsInNamespace implements PaginatedAction, KubeAction

export class GetKubernetesNamespaces implements KubePaginationAction {
constructor(public kubeGuid) {
this.paginationKey = getPaginationKey(kubernetesNamespacesEntityType, kubeGuid);
this.paginationKey = getPaginationKey(kubernetesNamespacesEntityType, kubeGuid || 'all');
}
type = GET_NAMESPACES_INFO;
entityType = kubernetesNamespacesEntityType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { catchError, combineLatest, flatMap, mergeMap } from 'rxjs/operators';
import { connectedEndpointsOfTypesSelector } from 'frontend/packages/store/src/selectors/endpoint.selectors';
import { of } from 'rxjs';
import { catchError, combineLatest, first, flatMap, map, mergeMap, switchMap } from 'rxjs/operators';

import { AppState } from '../../../../../store/src/app-state';
import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog.service';
Expand Down Expand Up @@ -281,6 +283,7 @@ export class KubernetesEffects {
ofType<GetKubernetesApps>(GET_KUBERNETES_APP_INFO),
flatMap(action => {
this.store.dispatch(new StartRequestAction(action));

const headers = new HttpHeaders({ 'x-cap-cnsi-list': action.kubeGuid });
const requestArgs = {
headers
Expand Down Expand Up @@ -365,32 +368,53 @@ export class KubernetesEffects {
}


private processListAction<T>(
private processListAction<T = any>(
action: KubePaginationAction | KubeAction,
url: string,
entityKey: string,
getId: GetID<T>,
filterResults?: Filter<T>) {
this.store.dispatch(new StartRequestAction(action));
const headers = new HttpHeaders({ 'x-cap-cnsi-list': action.kubeGuid });
const requestArgs = {
headers,
params: null
};
const paginationAction = action as KubePaginationAction;
if (paginationAction.initialParams) {
requestArgs.params = Object.keys(paginationAction.initialParams).reduce((httpParams, initialKey: string) => {
return httpParams.set(initialKey, paginationAction.initialParams[initialKey].toString());
}, new HttpParams());
}
return this.http
.get(url, requestArgs)
.pipe(mergeMap(response => {

const getKubeIds = action.kubeGuid ?
of([action.kubeGuid]) :
this.store.select(connectedEndpointsOfTypesSelector(KUBERNETES_ENDPOINT_TYPE)).pipe(
first(),
map(endpoints => Object.values(endpoints).map(endpoint => endpoint.guid))
);
let pKubeIds;

return getKubeIds.pipe(
switchMap(kubeIds => {
pKubeIds = kubeIds;
const headers = new HttpHeaders({ 'x-cap-cnsi-list': kubeIds });
// const headers = hr.headers.set(PipelineHttpClient.EndpointHeader, endpointGuids);
const requestArgs = {
headers,
params: null
};
const paginationAction = action as KubePaginationAction;
if (paginationAction.initialParams) {
requestArgs.params = Object.keys(paginationAction.initialParams).reduce((httpParams, initialKey: string) => {
return httpParams.set(initialKey, paginationAction.initialParams[initialKey].toString());
}, new HttpParams());
}
return this.http.get(url, requestArgs);
}),
mergeMap(allRes => {
const base = {
entities: { [entityKey]: {} },
result: []
} as NormalizedResponse;
const items = response[action.kubeGuid].items as Array<any>;

const items: Array<T> = Object.entries(allRes).reduce((combinedRes, [kubeId, res]) => {
res.items.forEach(item => {
item.metadata.kubeId = kubeId;
combinedRes.push(item);
});
return combinedRes;
}, []);
// const items = response[action.kubeGuid].items as Array<any>;
const processesData = items.filter((res) => !!filterResults ? filterResults(res) : true)
.reduce((res, data) => {
const id = getId(data);
Expand All @@ -401,15 +425,17 @@ export class KubernetesEffects {
return [
new WrapperRequestActionSuccess(processesData, action)
];
}), catchError(error => [
}),
catchError(error => [
new WrapperRequestActionFailed(error.message, action, 'fetch', {
endpointIds: [action.kubeGuid],
endpointIds: pKubeIds,
url: error.url || url,
eventCode: error.status ? error.status + '' : '500',
message: 'Kubernetes API request error',
error
error,
})
]));
])
);
}

private processSingleItemAction<T>(action: KubeAction, url: string, schemaKey: string, getId: GetID<T>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Injectable, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { safeUnsubscribe } from 'frontend/packages/core/src/core/utils.service';
import { AppState } from 'frontend/packages/store/src/app-state';
import { PaginationMonitorFactory } from 'frontend/packages/store/src/monitors/pagination-monitor.factory';
import {
getCurrentPageRequestInfo,
getPaginationObservables,
} from 'frontend/packages/store/src/reducers/pagination-reducer/pagination-reducer.helper';
import { connectedEndpointsOfTypesSelector } from 'frontend/packages/store/src/selectors/endpoint.selectors';
import { EndpointModel } from 'frontend/packages/store/src/types/endpoint.types';
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
import {
distinctUntilChanged,
filter,
first,
map,
publishReplay,
refCount,
startWith,
tap,
withLatestFrom,
} from 'rxjs/operators';

import { KUBERNETES_ENDPOINT_TYPE, kubernetesEntityFactory } from '../../kubernetes-entity-factory';
import { KubernetesNamespace } from '../../store/kube.types';
import { GetKubernetesNamespaces } from '../../store/kubernetes.actions';

export interface KubernetesNamespacesFilterItem<T = any> {
list$: Observable<T[]>;
loading$: Observable<boolean>;
select: BehaviorSubject<string>;
}

/**
* This service relies on OnDestroy, so must be `provided` by a component
*/
@Injectable()
export class KubernetesNamespacesFilterService implements OnDestroy {
public kube: KubernetesNamespacesFilterItem<EndpointModel>;
public namespace: KubernetesNamespacesFilterItem<KubernetesNamespace>;

private isLoading$: Observable<boolean>;
private subs: Subscription[] = [];

private paginationAction = new GetKubernetesNamespaces(null);
private allNamespaces = this.getNamespacesObservable();
private allNamespacesLoading$ = this.allNamespaces.pagination$.pipe(map(
pag => getCurrentPageRequestInfo(pag).busy
));

constructor(
private store: Store<AppState>,
private paginationMonitorFactory: PaginationMonitorFactory,
) {
this.kube = this.createKube();
this.namespace = this.createNamespace();

// Start watching the cf/org/space plus automatically setting values only when we actually have values to auto select
this.namespace.list$.pipe(first()).subscribe(() => this.setupAutoSelectors());

this.isLoading$ = combineLatest(
this.kube.loading$,
this.namespace.loading$,
).pipe(
map(([kubeLoading, nsLoading]) => kubeLoading || nsLoading)
);
}

private getNamespacesObservable() {
return getPaginationObservables<KubernetesNamespace>({
store: this.store,
action: this.paginationAction,
paginationMonitor: this.paginationMonitorFactory.create(
this.paginationAction.paginationKey,
kubernetesEntityFactory(this.paginationAction.entityType)
)
}, true);
}

private createKube() {
const list$ = this.store.select(connectedEndpointsOfTypesSelector(KUBERNETES_ENDPOINT_TYPE)).pipe(
// Ensure we have endpoints
filter(endpoints => endpoints && !!Object.keys(endpoints).length),
publishReplay(1),
refCount(),
);
return {
list$: list$.pipe(
map(endpoints => Object.values(endpoints)),
first(),
map((endpoints: EndpointModel[]) => {
return Object.values(endpoints).sort((a: EndpointModel, b: EndpointModel) => a.name.localeCompare(b.name));
}),
),
loading$: list$.pipe(map(kubes => !kubes)),
select: new BehaviorSubject(undefined)
};
}

private createNamespace() {
const namespaceList$ = combineLatest(
this.kube.select.asObservable(),
this.allNamespaces.entities$
).pipe(map(([selectedKubeId, entities]) => {
if (selectedKubeId && entities) {
return entities
.filter(namespace => namespace.metadata.kubeId === selectedKubeId)
.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
}
return [];
}));

return {
list$: namespaceList$,
loading$: this.allNamespacesLoading$,
select: new BehaviorSubject(undefined)
};
}

private setupAutoSelectors() {
const namespaceResetSub = this.kube.select.asObservable().pipe(
startWith(undefined),
distinctUntilChanged(),
withLatestFrom(this.namespace.list$),
tap(([, namespaces]) => {
if (!!namespaces.length && namespaces.length === 1
) {
this.selectSet(this.namespace.select, namespaces[0].metadata.name);
} else {
this.selectSet(this.namespace.select, undefined);
}
}),
).subscribe();
this.subs.push(namespaceResetSub);
}

private selectSet(select: BehaviorSubject<string>, newValue: string) {
if (select.getValue() !== newValue) {
select.next(newValue);
}
}

ngOnDestroy(): void {
this.destroy();
}

destroy() {
// OnDestroy will be called when the component the service is provided at is destroyed. In theory this should not need to be called
// separately, if you see error's first ensure the service is provided at a component that will be destroyed
// Should be called in the OnDestroy of the component where it's provided
safeUnsubscribe(...this.subs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,28 @@ import { ITableColumn } from 'frontend/packages/core/src/shared/components/list/
import {
TableCellEndpointNameComponent,
} from 'frontend/packages/core/src/shared/components/list/list-types/endpoint/table-cell-endpoint-name/table-cell-endpoint-name.component';
import { IListConfig, ListViewTypes } from 'frontend/packages/core/src/shared/components/list/list.component.types';
import {
IListConfig,
IListMultiFilterConfig,
ListViewTypes,
} from 'frontend/packages/core/src/shared/components/list/list.component.types';
import { AppState } from 'frontend/packages/store/src/app-state';
import { filter, map } from 'rxjs/operators';

import { defaultHelmKubeListPageSize } from '../../list-types/kube-helm-list-types';
import { HelmRelease } from '../workload.types';
import { KubernetesNamespacesFilterItem, KubernetesNamespacesFilterService } from './kube-namespaces-filter-config.service';
import { HelmReleasesDataSource } from './monocular-releases-list-source';

@Injectable()
export class HelmReleasesListConfig implements IListConfig<HelmRelease> {

isLocal = true;
dataSource: HelmReleasesDataSource;
viewType = ListViewTypes.TABLE_ONLY;
text = {
title: '',
filter: 'Filter Releases',
noEntries: 'There are no releases'
filter: 'Filter by Name',
};
pageSizeOptions = defaultHelmKubeListPageSize;
enableTextFilter = true;
Expand All @@ -42,7 +48,7 @@ export class HelmReleasesListConfig implements IListConfig<HelmRelease> {
orderKey: 'name',
field: 'name'
},
cellFlex: '2'
cellFlex: '2',
},
{
columnId: 'cluster',
Expand Down Expand Up @@ -104,18 +110,41 @@ export class HelmReleasesListConfig implements IListConfig<HelmRelease> {
},
];

private multiFilterConfigs: IListMultiFilterConfig[];

constructor(
private store: Store<AppState>,
public activatedRoute: ActivatedRoute,
private datePipe: DatePipe,
kubeNamespaceService: KubernetesNamespacesFilterService
) {
this.dataSource = new HelmReleasesDataSource(this.store, this);

this.multiFilterConfigs = [
createKubeNamespaceFilterConfig('kubeId', 'Kubernetes', kubeNamespaceService.kube),
createKubeNamespaceFilterConfig('namespace', 'Namespace', kubeNamespaceService.namespace),
];
}

public getColumns = () => this.columns;
public getGlobalActions = () => [];
public getMultiActions = () => [];
public getSingleActions = () => [];
public getMultiFiltersConfigs = () => [];
getMultiFiltersConfigs = () => this.multiFilterConfigs;
public getDataSource = () => this.dataSource;
}

function createKubeNamespaceFilterConfig(key: string, label: string, cfOrgSpaceItem: KubernetesNamespacesFilterItem) {
return {
key,
label,
...cfOrgSpaceItem,
list$: cfOrgSpaceItem.list$.pipe(map((entities: any[]) => {
return entities.map(entity => ({
label: entity.name || entity.metadata.name,
item: entity,
value: entity.guid || entity.metadata.name // Endpoint search via guid, namespace by name (easier filtering)
}));
})),
};
}
Loading