import { isEmpty, pickBy } from 'lodash'; import React from 'react'; import type { Observable } from 'rxjs'; import { empty as observableEmpty, Subject } from 'rxjs'; import { distinctUntilChanged, map, scan, switchMap, takeUntil, tap } from 'rxjs/operators'; import { RecentlyViewedItems } from '../infrastructure/RecentlyViewedItems'; import { SearchResultPods } from '../infrastructure/SearchResultPods'; import type { ISearchResultSet } from '../infrastructure/infrastructureSearch.service'; import { InfrastructureSearchServiceV2 } from '../infrastructure/infrastructureSearchV2.service'; import { InsightMenu } from '../../insight/InsightMenu'; import type { IQueryParams } from '../../navigation'; import { ReactInjector } from '../../reactShims'; import { SearchResults, searchResultTypeRegistry, SearchStatus } from '../searchResult'; import type { ITag } from '../../widgets'; import { Search } from '../widgets'; // These state parameters are passed through to Gate's search API const API_PARAMS = ['key', 'name', 'account', 'region', 'stack']; export interface ISearchV2State { selectedTab: string; params: { [key: string]: any }; resultSets: ISearchResultSet[]; isSearching: boolean; refreshingCache: boolean; } export class SearchV2 extends React.Component<{}, ISearchV2State> { private $state = ReactInjector.$state; private $uiRouter = ReactInjector.$uiRouter; private searchResultTypes = searchResultTypeRegistry.getAll(); private INITIAL_RESULTS: ISearchResultSet[] = this.searchResultTypes.map((type) => ({ type, status: SearchStatus.SEARCHING, results: [], })); private destroy$ = new Subject(); constructor(props: {}) { super(props); this.state = { selectedTab: this.$state.params.tab, params: {}, resultSets: this.INITIAL_RESULTS, isSearching: false, refreshingCache: false, }; // just set the page title - don't try to get fancy w/ the search terms ReactInjector.pageTitleService.handleRoutingSuccess({ pageTitleMain: { field: undefined, label: 'Search' } }); } // returns parameter values that are OK to send through to the back end search API as filters private getApiFilterParams(params: IQueryParams): IQueryParams { const isValidApiParam = (val: any, key: string) => { return ( API_PARAMS.includes(key) && val !== null && val !== undefined && !(typeof val === 'string' && val.trim() === '') ); }; return pickBy(params, isValidApiParam); } public componentDidMount() { // auto-navigation only happens via shortcut links, and we only do it if there is exactly one result, e.g // when searching for an instance ID const autoNavigate = window.location.href.endsWith('route=true'); this.$uiRouter.globals.params$ .pipe( map((stateParams) => this.getApiFilterParams(stateParams)), tap((params: IQueryParams) => this.setState({ params })), distinctUntilChanged((a, b) => API_PARAMS.every((key) => a[key] === b[key])), tap(() => this.setState({ resultSets: this.INITIAL_RESULTS, isSearching: true })), // Got new params... fire off new queries for each backend // Use switchMap so new queries cancel any pending previous queries switchMap( (params: IQueryParams): Observable => { if (isEmpty(params)) { return observableEmpty(); } // Start fetching results for each search type from the search service. // Update the overall results with the results for each search type. return InfrastructureSearchServiceV2.search({ ...params }).pipe( scan((acc: ISearchResultSet[], resultSet: ISearchResultSet): ISearchResultSet[] => { const status = resultSet.status === SearchStatus.SEARCHING ? SearchStatus.FINISHED : resultSet.status; resultSet = { ...resultSet, status }; // Replace the result set placeholder with the results for this type return acc.filter((set) => set.type !== resultSet.type).concat(resultSet); }, this.INITIAL_RESULTS), ); }, ), takeUntil(this.destroy$), ) .subscribe( (resultSets) => { const finishedSearching = resultSets.map((r) => r.status).every((s) => s === SearchStatus.FINISHED); if (finishedSearching && autoNavigate) { const allResults = resultSets.reduce((acc, rs) => acc.concat(rs.results), []); if (allResults.length === 1) { window.location.href = allResults[0].href; return; } } if (!this.state.selectedTab) { this.selectTab(resultSets); } this.setState({ resultSets, isSearching: !finishedSearching }); }, null, () => this.setState({ isSearching: false }), ); this.$uiRouter.globals.params$ .pipe( map((params) => params.tab), distinctUntilChanged(), takeUntil(this.destroy$), ) .subscribe((selectedTab) => this.setState({ selectedTab })); } /** Select the first tab with results */ private selectTab(resultSets: ISearchResultSet[]): void { // Prioritize applications tab over all others const order = (rs: ISearchResultSet) => (rs.type.id === 'applications' ? -1 : rs.type.order); const tabs = resultSets.slice().sort((a, b) => order(a) - order(b)); // Scan all tabs in order. Find the first tab that has results. Stop scanning when a tab with unfinished results is encountered. const found = tabs.reduce( (previous, tab) => { const resultAlreadyFound = previous.tabId || previous.unfinished; const unfinished = tab.status !== SearchStatus.FINISHED; const tabId = tab.results.length ? tab.type.id : null; return resultAlreadyFound ? previous : { ...previous, unfinished, tabId }; }, { tabId: null, unfinished: false }, ); if (found.tabId) { this.$state.go('.', { tab: found.tabId }); } } public componentWillUnmount() { this.destroy$.next(); } public handleFilterChange = (filters: ITag[]) => { const blankApiParams = API_PARAMS.reduce((acc, key) => ({ ...acc, [key]: undefined }), {}); const newParams = filters.reduce((params, filter) => ({ ...params, [filter.key]: filter.text }), blankApiParams); this.$state.go('.', newParams, { location: 'replace' }); }; public render() { const { params, resultSets, selectedTab, isSearching } = this.state; const hasSearchQuery = Object.keys(params).length > 0; return (

{!hasSearchQuery && (
)} {hasSearchQuery && (
)}
); } }