import { Component, Inject, OnInit, OnDestroy, ChangeDetectorRef, ChangeDetectionStrategy, NgZone, ViewEncapsulation } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatDividerModule } from '@angular/material/divider'; import { MatListModule } from '@angular/material/list'; import { MatSelectModule } from '@angular/material/select'; import { MatFormFieldModule } from '@angular/material/form-field'; import { BreakpointObserver } from '@angular/cdk/layout'; import { MatInputModule } from '@angular/material/input'; import { I18nService } from '../../services/i18n.service'; import { SocketService } from '../../services/socket.service'; import { CommonService } from '../../services/common.service'; import { P3xrAccordionComponent } from '../../components/p3xr-accordion.component'; import { P3xrButtonComponent } from '../../components/p3xr-button.component'; declare const p3xr: any; require('./search.component.scss'); @Component({ selector: 'p3xr-search', standalone: true, imports: [ CommonModule, FormsModule, MatIconModule, MatButtonModule, MatTooltipModule, MatDividerModule, MatListModule, MatSelectModule, MatFormFieldModule, MatInputModule, P3xrAccordionComponent, P3xrButtonComponent, ], templateUrl: './search.component.html', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) export class SearchComponent implements OnInit, OnDestroy { strings; indexes: string[] = []; selectedIndex = ''; query = '*'; offset = 0; limit = 20; total = 0; results: any[] = []; indexInfo: any = null; searching = false; searchDone = false; isReadonly = false; isGtSm = true; // Index creation newIndexName = ''; newIndexPrefix = ''; newIndexFields: Array<{ name: string; type: string; sortable: boolean }> = [ { name: '', type: 'TEXT', sortable: false }, ]; private unsubs: Array<() => void> = []; constructor( @Inject(I18nService) private i18n: I18nService, @Inject(SocketService) private socket: SocketService, @Inject(CommonService) private common: CommonService, @Inject(ChangeDetectorRef) private cdr: ChangeDetectorRef, @Inject(NgZone) private ngZone: NgZone, @Inject(BreakpointObserver) private breakpointObserver: BreakpointObserver, ) { this.strings = this.i18n.strings; } ngOnInit(): void { this.isReadonly = p3xr?.state?.connection?.readonly === true; const sub960 = this.breakpointObserver.observe('(min-width: 960px)').subscribe(r => { this.isGtSm = r.matches; this.cdr.markForCheck(); }); this.unsubs.push(() => sub960.unsubscribe()); this.loadIndexes(); const sub = this.socket.stateChanged$.subscribe(() => { this.isReadonly = p3xr?.state?.connection?.readonly === true; this.loadIndexes(); }); this.unsubs.push(() => sub.unsubscribe()); } async searchAndRefreshInfo(): Promise { await Promise.all([ this.search(), this.loadIndexInfo(), ]); } ngOnDestroy(): void { this.unsubs.forEach(fn => fn()); } get pages(): number { return Math.ceil(this.total / this.limit); } get currentPage(): number { return Math.floor(this.offset / this.limit) + 1; } async loadIndexes(): Promise { try { const response = await this.socket.request({ action: 'search-list', payload: {} }); this.indexes = response.data; if (this.indexes.length > 0 && !this.selectedIndex) { this.selectedIndex = this.indexes[0]; } this.cdr.markForCheck(); } catch { /* ignore */ } } async search(): Promise { if (!this.selectedIndex || !this.query) return; this.searching = true; this.cdr.markForCheck(); try { const response = await this.socket.request({ action: 'search-query', payload: { index: this.selectedIndex, query: this.query, offset: this.offset, limit: this.limit, }, }); this.total = response.data.total; this.results = response.data.docs; } catch (e) { this.common.generalHandleError(e); this.results = []; this.total = 0; } finally { this.searching = false; this.searchDone = true; this.cdr.markForCheck(); } } pageAction(action: string): void { switch (action) { case 'first': this.offset = 0; break; case 'prev': this.offset = Math.max(0, this.offset - this.limit); break; case 'next': this.offset = Math.min((this.pages - 1) * this.limit, this.offset + this.limit); break; case 'last': this.offset = (this.pages - 1) * this.limit; break; } this.search(); } async loadIndexInfo(): Promise { if (!this.selectedIndex) return; try { const response = await this.socket.request({ action: 'search-index-info', payload: { index: this.selectedIndex }, }); this.indexInfo = response.data; this.cdr.markForCheck(); } catch (e) { this.common.generalHandleError(e); } } async dropIndex(): Promise { if (!this.selectedIndex) return; try { await this.common.confirm({ message: this.strings().confirm?.dropIndex || 'Are you sure to drop this index?', }); await this.socket.request({ action: 'search-index-drop', payload: { index: this.selectedIndex }, }); this.common.toast({ message: this.strings().status?.indexDropped || 'Index dropped' }); this.selectedIndex = ''; this.results = []; this.total = 0; this.indexInfo = null; await this.loadIndexes(); } catch (e) { if (e !== undefined) this.common.generalHandleError(e); } } addField(): void { this.newIndexFields.push({ name: '', type: 'TEXT', sortable: false }); } async confirmRemoveField(index: number): Promise { try { const label = this.strings().intention?.delete || 'Delete'; await this.common.confirm({ message: label + '?' }); this.newIndexFields.splice(index, 1); } catch (e) { if (e !== undefined) this.common.generalHandleError(e); } } async createIndex(): Promise { if (!this.newIndexName.trim()) return; const schema = this.newIndexFields.filter(f => f.name.trim()); if (schema.length === 0) return; try { await this.socket.request({ action: 'search-index-create', payload: { name: this.newIndexName.trim(), prefix: this.newIndexPrefix.trim() || undefined, schema, }, }); this.common.toast({ message: this.strings().status?.indexCreated || 'Index created' }); this.newIndexName = ''; this.newIndexPrefix = ''; this.newIndexFields = [{ name: '', type: 'TEXT', sortable: false }]; await this.loadIndexes(); } catch (e) { this.common.generalHandleError(e); } } getDocKeys(doc: any): string[] { return Object.keys(doc).filter(k => k !== '_key'); } }