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'; 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 || 'Key' }} @if (keyForm.controls['key']?.invalid && keyForm.controls['key']?.touched) { {{ strings().form?.key?.error?.key }} } {{ strings().form?.key?.field?.type || 'Type' }} @for (t of types; track t) { {{ strings().redisTypes?.[t] || t }} } @switch (model.type) { @case ('list') { {{ strings().form?.key?.field?.index || 'Index' }}
{{ strings().label?.redisListIndexInfo }}
} @case ('hash') { {{ strings().form?.key?.field?.hashKey || 'Hash Key' }} @if (keyForm.controls['hashKey']?.invalid && keyForm.controls['hashKey']?.touched) { {{ strings().form?.key?.error?.hashKey }} } } @case ('zset') { {{ strings().form?.key?.field?.score || 'Score' }} @if (keyForm.controls['score']?.invalid && keyForm.controls['score']?.touched) { {{ strings().form?.key?.error?.score }} } } @case ('stream') { {{ strings().form?.key?.field?.streamTimestamp || 'Timestamp' }} @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 || 'Retention' }} (ms) {{ strings().page?.key?.timeseries?.retentionHint || '0 = no expiry, or milliseconds' }} {{ strings().page?.key?.timeseries?.duplicatePolicy || 'Duplicate policy' }} LAST FIRST MIN MAX SUM BLOCK } {{ strings().page?.key?.timeseries?.labels || 'Labels' }} {{ strings().page?.key?.timeseries?.labelsHint || 'key1 value1 key2 value2' }} @if (!model.tsBulkMode) { {{ strings().page?.key?.timeseries?.timestamp || 'Timestamp' }} {{ strings().page?.key?.timeseries?.timestampHint || "'*' means auto generated, or milliseconds timestamp" }} } @if (model.originalTimestamp === undefined) { {{ strings().page?.key?.timeseries?.bulkMode || 'Bulk generate' }} } } } @if (model.type !== 'stream' && model.type !== 'timeseries') { } @if (model.type !== 'timeseries') { }
@if (model.type !== 'timeseries') { {{ strings().label?.validateJson || 'Validate JSON' }} @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 || 'Auto * spread' }} 1 {{ strings().time?.second || 'second' }} 30 {{ strings().time?.seconds || 'seconds' }} 1 {{ strings().time?.minute || 'minute' }} 30 {{ strings().time?.minutes || 'minutes' }} 1 {{ strings().time?.hour || 'hour' }} 24 {{ strings().time?.hours || 'hours' }} {{ strings().page?.key?.timeseries?.formula || 'Formula' }} {{ strings().page?.key?.timeseries?.none || 'None' }} sin cos {{ strings().page?.key?.timeseries?.formulaLinear || 'Linear' }} {{ strings().page?.key?.timeseries?.formulaRandom || 'Random' }} {{ strings().page?.key?.timeseries?.formulaSawtooth || 'Sawtooth' }}
@if (model.tsFormula) {
{{ strings().page?.key?.timeseries?.formulaPoints || 'Points' }} {{ strings().page?.key?.timeseries?.formulaAmplitude || 'Amplitude' }} {{ strings().page?.key?.timeseries?.formulaOffset || 'Offset' }}
} {{ strings().page?.key?.timeseries?.dataPoints || 'data points' }} {{ strings().page?.key?.timeseries?.editAllHint || 'One data point per line: timestamp value (timestamp can be * for auto)' }} @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 || 'Value' }} @if (keyForm.controls['value']?.invalid && keyForm.controls['value']?.touched) { {{ strings().form?.key?.error?.value }} } } @else { {{ strings().form?.key?.field?.value || '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'); } return base; } 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, ) { 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: '*', tsTimestamp: '*', tsRetention: 0, tsDuplicatePolicy: 'LAST', tsLabels: '', tsBulkMode: false, tsSpread: 60000, tsFormula: '', tsFormulaPoints: 25, tsFormulaAmplitude: 100, tsFormulaOffset: 0, hashKey: undefined, index: undefined, }; 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 || 'Edit Key'; if (this.options.type === 'append') return s.form?.key?.label?.formName?.append || 'Append'; return s.form?.key?.label?.formName?.add || 'Add Key'; } 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 || 'Copied'); } 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 || 'Not valid JSON'); } } 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 || 'Upload buffer?' }); const arrayBuffer = await file.arrayBuffer(); this.model.value = arrayBuffer; this.isBuffer = true; this.common.toast(this.strings().confirm?.uploadBufferDone || 'Buffer uploaded'); } 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 || 'Key cannot be empty'); return; } if (this.validateJson) { try { JSON.parse(this.model.value); } catch (e) { this.common.toast(this.strings().label?.jsonViewNotParsable || 'Not valid JSON'); return; } } try { 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 || 'Saved'); 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); } }