import { Component, Inject, OnInit, ChangeDetectorRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { DialogCancelButtonComponent } from '../components/dialog-cancel-button.component'; import { BreakpointObserver } from '@angular/cdk/layout'; import { I18nService } from '../services/i18n.service'; import { CommonService } from '../services/common.service'; import { SocketService } from '../services/socket.service'; import { RedisStateService } from '../services/redis-state.service'; import { SettingsService } from '../services/settings.service'; import { JsonViewDialogService } from './json-view-dialog.service'; import { JsonEditorDialogService } from './json-editor-dialog.service'; import { OverlayService } from '../services/overlay.service'; import { DiffDialogService } from './diff-dialog.service'; export interface KeyNewOrSetDialogData { type: 'add' | 'edit' | 'append'; $event?: any; node?: any; model?: any; } /** * Key New/Edit dialog — Angular replacement for p3xrDialogKeyNewOrSet. * Multi-type form for creating or editing Redis keys (string, list, hash, set, zset, stream). */ @Component({ selector: 'p3xr-key-new-or-set-dialog', standalone: true, imports: [ CommonModule, FormsModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatSlideToggleModule, MatButtonModule, MatIconModule, MatToolbarModule, MatTooltipModule, DialogCancelButtonComponent, ], template: `
{{ getTitle() }} {{ strings().form?.key?.field?.key }} @if (keyForm.controls['key']?.invalid && keyForm.controls['key']?.touched) { {{ strings().form?.key?.error?.key }} } {{ strings().form?.key?.field?.type }} @for (t of types; track t) { {{ strings().redisTypes?.[t] || t }} } @switch (model.type) { @case ('list') { {{ strings().form?.key?.field?.index }}
{{ strings().label?.redisListIndexInfo }}
} @case ('hash') { {{ strings().form?.key?.field?.hashKey }} @if (keyForm.controls['hashKey']?.invalid && keyForm.controls['hashKey']?.touched) { {{ strings().form?.key?.error?.hashKey }} } } @case ('zset') { {{ strings().form?.key?.field?.score }} @if (keyForm.controls['score']?.invalid && keyForm.controls['score']?.touched) { {{ strings().form?.key?.error?.score }} } } @case ('stream') { {{ strings().form?.key?.field?.streamTimestamp }} @if (keyForm.controls['streamTimestamp']?.invalid && keyForm.controls['streamTimestamp']?.touched) { {{ strings().form?.key?.error?.streamTimestamp }} }
{{ strings().label?.streamTimestampId }}
} @case ('timeseries') { @if (options.type === 'add') { {{ strings().page?.key?.timeseries?.retention }} (ms) {{ strings().page?.key?.timeseries?.retentionHint }} {{ strings().page?.key?.timeseries?.duplicatePolicy }} LAST FIRST MIN MAX SUM BLOCK } {{ strings().page?.key?.timeseries?.labels }} {{ strings().page?.key?.timeseries?.labelsHint }} @if (!model.tsBulkMode) { {{ strings().page?.key?.timeseries?.timestamp }} {{ strings().page?.key?.timeseries?.timestampHint }} } @if (model.originalTimestamp === undefined) { {{ strings().page?.key?.timeseries?.bulkMode }} } } @case ('bloom') {
{{ strings().form?.key?.field?.errorRate }} {{ strings().form?.key?.field?.capacity }}
} @case ('cuckoo') { {{ strings().form?.key?.field?.capacity }} } @case ('topk') {
Top K {{ strings().form?.key?.field?.width }} {{ strings().form?.key?.field?.depth }} {{ strings().form?.key?.field?.decay }}
} @case ('cms') {
{{ strings().form?.key?.field?.width }} {{ strings().form?.key?.field?.depth }}
} @case ('tdigest') { {{ strings().form?.key?.field?.compression }} } @case ('vectorset') { {{ strings().page?.key?.vectorset?.elementName }} {{ strings().page?.key?.vectorset?.vectorValues }} } } @if (model.type !== 'stream' && model.type !== 'timeseries' && !isProbabilisticType()) { } @if (model.type !== 'timeseries' && !isProbabilisticType()) { }
@if (model.type !== 'timeseries' && !isProbabilisticType()) { {{ strings().label?.validateJson }} @if (model.type === 'stream') {
{{ strings().label?.streamValue }}
} @if (isBuffer) {
{{ strings().label?.isBuffer?.({ maxValueAsBuffer: getMaxValueAsBufferText() }) }} {{ bufferDisplay(model.value) }}
} } @if (model.type === 'timeseries' && (model.tsEditAll || model.tsBulkMode)) {
{{ strings().page?.key?.timeseries?.autoSpread }} 1 {{ strings().time?.second }} 30 {{ strings().time?.seconds }} 1 {{ strings().time?.minute }} 30 {{ strings().time?.minutes }} 1 {{ strings().time?.hour }} 24 {{ strings().time?.hours }} {{ strings().page?.key?.timeseries?.formula }} {{ strings().page?.key?.timeseries?.none }} sin cos {{ strings().page?.key?.timeseries?.formulaLinear }} {{ strings().page?.key?.timeseries?.formulaRandom }} {{ strings().page?.key?.timeseries?.formulaSawtooth }}
@if (model.tsFormula) {
{{ strings().page?.key?.timeseries?.formulaPoints }} {{ strings().page?.key?.timeseries?.formulaAmplitude }} {{ strings().page?.key?.timeseries?.formulaOffset }}
} {{ strings().page?.key?.timeseries?.dataPoints }} {{ strings().page?.key?.timeseries?.editAllHint }} @if (keyForm.controls['value']?.invalid && keyForm.controls['value']?.touched) { {{ strings().form?.key?.error?.value }} } } @else if (model.type === 'timeseries' && !model.tsBulkMode) { {{ strings().page?.key?.timeseries?.value }} @if (keyForm.controls['value']?.invalid && keyForm.controls['value']?.touched) { {{ strings().form?.key?.error?.value }} } } @else if (!isProbabilisticType()) { {{ strings().form?.key?.field?.value }} @if (keyForm.controls['value']?.invalid && keyForm.controls['value']?.touched) { {{ strings().form?.key?.error?.value }} } }
@if (!isReadonly) { }
`, styles: [` .full-width { width: 100%; } .info-text { opacity: 0.5; font-size: 12px; margin-bottom: 8px; } .hide-sm { display: inline; } @media (max-width: 959px) { .hide-sm { display: none; } } `], }) export class KeyNewOrSetDialogComponent implements OnInit { model: any = {}; options: KeyNewOrSetDialogData; get types(): string[] { const base = ['string', 'list', 'hash', 'set', 'zset', 'stream']; if (this.state.hasTimeSeries()) { base.push('timeseries'); } if (this.state.hasReJSON()) { base.push('json'); } if (this.state.hasBloom()) { base.push('bloom', 'cuckoo', 'topk', 'cms', 'tdigest'); } base.push('vectorset'); return base; } private static readonly PROBABILISTIC_TYPES = ['bloom', 'cuckoo', 'topk', 'cms', 'tdigest']; isProbabilisticType(): boolean { return KeyNewOrSetDialogComponent.PROBABILISTIC_TYPES.includes(this.model.type) || this.model.type === 'vectorset'; } validateJson = false; isReadonly = false; isBuffer = false; isWide = window.innerWidth >= 720; strings; constructor( @Inject(MatDialogRef) private dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) private data: KeyNewOrSetDialogData, @Inject(I18nService) private i18n: I18nService, @Inject(CommonService) private common: CommonService, @Inject(SocketService) private socket: SocketService, @Inject(JsonViewDialogService) private jsonViewDialog: JsonViewDialogService, @Inject(JsonEditorDialogService) private jsonEditorDialog: JsonEditorDialogService, @Inject(BreakpointObserver) private breakpointObserver: BreakpointObserver, @Inject(ChangeDetectorRef) private cdr: ChangeDetectorRef, @Inject(RedisStateService) private state: RedisStateService, @Inject(SettingsService) private settings: SettingsService, @Inject(OverlayService) private overlay: OverlayService, @Inject(DiffDialogService) private diffDialog: DiffDialogService, ) { this.strings = this.i18n.strings; this.options = data; } ngOnInit(): void { this.isReadonly = this.state.connection()?.readonly === true; this.breakpointObserver.observe('(min-width: 720px)').subscribe(r => { this.isWide = r.matches; this.cdr.markForCheck(); }); this.model = { type: 'string', key: this.data.node?.key ? this.data.node.key + (this.settings.redisTreeDivider() ?? ':') : '', value: undefined, score: undefined, streamTimestamp: undefined, tsTimestamp: undefined, tsRetention: 0, tsDuplicatePolicy: 'LAST', tsLabels: '', tsBulkMode: false, tsSpread: 60000, tsFormula: '', tsFormulaPoints: 25, tsFormulaAmplitude: 100, tsFormulaOffset: 0, hashKey: undefined, index: undefined, bloomErrorRate: 0.01, bloomCapacity: 100, cuckooCapacity: 1024, topkK: 10, topkWidth: 2000, topkDepth: 7, topkDecay: 0.9, cmsWidth: 2000, cmsDepth: 7, tdigestCompression: 100, vectorElement: '', vectorValues: '', }; if (this.data.model) { Object.assign(this.model, this.data.model); } this.isBuffer = typeof this.model.value === 'object' && this.model.value !== null; } getTitle(): string { const s = this.strings(); if (this.options.type === 'edit') return s.form?.key?.label?.formName?.edit; if (this.options.type === 'append') return s.form?.key?.label?.formName?.append; return s.form?.key?.label?.formName?.add; } getMaxValueAsBufferText(): string { try { return this.settings.prettyBytes(this.settings.maxValueAsBuffer); } catch { return `${this.settings.maxValueAsBuffer} bytes`; } } bufferDisplay(value: any): string { if (value?.byteLength !== undefined) { return '(' + this.settings.prettyBytes(value.byteLength) + ')'; } return ''; } async copy(): Promise { let value = this.model.value; if (this.model.type === 'timeseries') { value = `TS.ADD ${this.model.key} ${this.model.tsTimestamp || '*'} ${this.model.value}`; } await this.settings.clipboard(value); this.common.toast(this.strings().status?.dataCopied); } async openJsonViewer(): Promise { await this.jsonViewDialog.show({ value: this.model.value }); } async openJsonEditor(): Promise { try { const result = await this.jsonEditorDialog.show({ value: this.model.value }); this.model.value = result.obj; } catch (e) { /* cancelled */ } } formatJson(): void { try { this.model.value = JSON.stringify(JSON.parse(this.model.value), null, this.settings.jsonFormat() ?? 2); } catch (e) { this.common.toast(this.strings().label?.jsonViewNotParsable); } } async onFileSelected(event: Event): Promise { const input = event.target as HTMLInputElement; const file = input.files?.[0]; if (!file) return; try { await this.common.confirm({ message: this.strings().confirm?.uploadBuffer }); const arrayBuffer = await file.arrayBuffer(); this.model.value = arrayBuffer; this.isBuffer = true; this.common.toast(this.strings().confirm?.uploadBufferDone); } catch (e) { /* cancelled */ } input.value = ''; } async submit(): Promise { if (!this.model.key || this.model.key.trim().length === 0) { this.common.toast(this.strings().form?.key?.error?.key); return; } if (this.validateJson) { try { JSON.parse(this.model.value); } catch (e) { this.common.toast(this.strings().label?.jsonViewNotParsable); return; } } try { // Show diff for edits (not new keys) if (this.data.model?.value !== undefined && this.data.model.value !== this.model.value) { const confirmed = await this.diffDialog.show({ keyName: this.model.key, fieldName: this.model.hashKey || undefined, oldValue: String(this.data.model.value), newValue: String(this.model.value), }); if (!confirmed) return; } this.overlay.show(); const response = await this.socket.request({ action: 'key/new-or-set', payload: { type: this.options.type, originalValue: this.data.model?.value, originalHashKey: this.data.model?.hashKey, model: structuredClone(this.model), }, }); if (typeof window['gtag'] === 'function') { window['gtag']('config', this.settings.googleAnalytics, { page_path: '/key-new-or-set' }); } this.common.toast(this.strings().status?.set); this.dialogRef.close(response); } catch (e) { this.common.generalHandleError(e); } finally { this.overlay.hide(); } } generateFormula(): void { const points = Math.min(Math.max(parseInt(this.model.tsFormulaPoints) || 25, 1), 10000); const amplitude = parseFloat(this.model.tsFormulaAmplitude) || 100; const offset = parseFloat(this.model.tsFormulaOffset) || 0; const formula = this.model.tsFormula; const lines: string[] = []; for (let i = 0; i < points; i++) { const x = i / points; let value: number; switch (formula) { case 'sin': value = Math.sin(x * Math.PI * 2) * amplitude + offset; break; case 'cos': value = Math.cos(x * Math.PI * 2) * amplitude + offset; break; case 'linear': value = x * amplitude + offset; break; case 'random': value = Math.random() * amplitude + offset; break; case 'sawtooth': value = (x % 0.25) * 4 * amplitude + offset; break; default: value = offset; } lines.push(`* ${parseFloat(value.toFixed(4))}`); } this.model.value = lines.join('\n'); } cancel(): void { this.dialogRef.close(undefined); } }