import { isEqual } from 'lodash'; import React from 'react'; import type { SelectCallback } from 'react-bootstrap'; import { BehaviorSubject, from as observableFrom, Subject } from 'rxjs'; import { combineLatest as observableCombineLatest } from 'rxjs'; import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; import { ApplicationTable } from './ApplicationsTable'; import { PaginationControls } from './PaginationControls'; import type { IAccount } from '../../account'; import type { Application } from '../../application'; import type { ICache } from '../../cache'; import { ViewStateCache } from '../../cache'; import { InsightMenu } from '../../insight/InsightMenu'; import { ModalInjector, ReactInjector } from '../../reactShims'; import type { IApplicationSummary } from '../service/ApplicationReader'; import { ApplicationReader } from '../service/ApplicationReader'; import { Spinner } from '../../widgets'; import '../applications.less'; interface IViewState { filter: string; sort: string; } interface IApplicationsStateParams { create?: string; } export interface IApplicationPagination { currentPage: number; itemsPerPage: number; maxSize: number; } export interface IApplicationsState { accounts: IAccount[]; applications: IApplicationSummary[]; errorState: boolean; pagination: IApplicationPagination; } export class Applications extends React.Component<{}, IApplicationsState> { private applicationsCache: ICache; private filter$ = new BehaviorSubject(null); private sort$ = new BehaviorSubject(null); private pagination$ = new BehaviorSubject(this.getDefaultPagination()); private destroy$ = new Subject(); constructor(props: {}) { super(props); this.applyCachedViewState(); const pagination = this.getDefaultPagination(); this.state = { pagination } as IApplicationsState; } public componentWillUnmount(): void { this.destroy$.next(); } private applyCachedViewState() { this.applicationsCache = ViewStateCache.get('applications') || ViewStateCache.createCache('applications', { version: 2 }); const viewState: IViewState = this.applicationsCache.get('#global') || { filter: '', sort: '+name' }; this.filter$.next(viewState.filter); this.sort$.next(viewState.sort); } public componentDidMount() { const appMatchesQuery = (query: string, app: IApplicationSummary) => { const searchableValues = [app.name, app.email, app.accounts, app.description] .filter((f) => !!f) .map((f) => f.toLowerCase()); return searchableValues.some((value) => value.includes(query)); }; const appSort = (column: string, a: any, b: any) => { const reverse = column[0] === '-'; const key = reverse ? column.slice(1) : column; return (a[key] || '').localeCompare(b[key] || '') * (reverse ? -1 : 1); }; const applicationSummaries$ = observableFrom(ApplicationReader.listApplications()).pipe( map((apps) => apps.map((app) => this.fixAccount(app))), ); const filteredApps$ = observableCombineLatest([applicationSummaries$, this.filter$, this.sort$]).pipe( // Apply filter/sort map(([apps, filter, sort]) => { const viewState: IViewState = { filter, sort }; this.applicationsCache.put('#global', viewState); return apps.filter((app) => appMatchesQuery(filter, app)).sort((a, b) => appSort(sort, a, b)); }), ); // validate and update pagination observableCombineLatest([filteredApps$, this.pagination$.pipe(distinctUntilChanged(isEqual))]) .pipe( map(([applications, pagination]) => { const lastPage = Math.floor(applications.length / pagination.itemsPerPage) + 1; const currentPage = Math.min(pagination.currentPage, lastPage); const maxSize = applications.length; const validatedPagination = { ...pagination, currentPage, maxSize } as IApplicationPagination; return { applications, pagination: validatedPagination }; }), takeUntil(this.destroy$), ) .subscribe( ({ applications, pagination }) => { const { currentPage, itemsPerPage } = pagination; const start = (currentPage - 1) * itemsPerPage; const end = start + itemsPerPage; this.setState({ applications: applications.slice(start, end), pagination }); }, (error) => { this.setState({ errorState: true }); throw error; }, ); const { $stateParams, $state, $rootScope, overrideRegistry } = ReactInjector; const { create } = $stateParams as IApplicationsStateParams; applicationSummaries$.subscribe((applications: IApplicationSummary[]) => { if (create) { const found = applications.find((app) => app.name === create); if (found) { if (found.email) { // Application already exists - redirect to app $state.go('home.applications.application', { application: create, create: null }); } else { // Inferred application - redirect to config $state.go('home.applications.application.config', { application: create, create: null }); } } else { // Nonexistant application - open create modal ModalInjector.modalService .open({ scope: $rootScope.$new(), templateUrl: overrideRegistry.getTemplate( 'createApplicationModal', require('../modal/newapplication.html'), ), resolve: { name: () => create, }, controller: overrideRegistry.getController('CreateApplicationModalCtrl'), controllerAs: 'newAppModal', }) .result.then( (app: Application) => { $state.go('home.applications.application', { application: app.name, create: null }); }, () => { // Clear out the query parameter if the dialog is dismissed $state.go('home.applications', { create: null }); }, ) .catch(() => {}); } } }); } private toggleSort(column: string): void { const current = this.sort$.getValue(); const newSort = current === column ? `-${column}` : column; this.sort$.next(newSort); } private fixAccount(application: IApplicationSummary): IApplicationSummary { if (application.accounts) { application.accounts = application.accounts.split(',').sort().join(', '); } return application; } private getDefaultPagination(): IApplicationPagination { return { currentPage: 1, itemsPerPage: 12, maxSize: 12, }; } public render() { const { applications, pagination, errorState } = this.state; const { maxSize, currentPage, itemsPerPage } = pagination; const currentSort = this.sort$.value; const changePage: SelectCallback = (page: any) => { return this.pagination$.next({ ...pagination, currentPage: page }); }; const LoadingSpinner = () => (
); const ErrorIndicator = () => (
Error fetching applications. Check that your gate endpoint is accessible. Further information on troubleshooting this error is available here.
); const ApplicationData = () => { if (errorState) { return ; } if (!applications) { return ; } if (applications.length === 0) { return

No matches found for '{this.filter$.value}'

; } return (
this.toggleSort(column)} />
); }; return (

Applications input && input.focus()} onChange={(evt) => this.filter$.next(evt.target.value)} value={this.filter$.value} />

); } }