Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<mat-dialog-content>
<h2>Message Attributes</h2>
<div class="dialog-header">
<h2>Message Attributes</h2>
<button mat-stroked-button (click)="toggleJsonMode()">
{{ jsonMode ? 'Key-Value' : 'JSON' }}
<mat-icon>{{ jsonMode ? 'list' : 'data_object' }}</mat-icon>
</button>
</div>

@if(this.attributes){
@if(!jsonMode) {
@for(entry of this.attributes | keyvalue; track entry.key){
<div class="row">
<div class="key">{{entry.key}}</div>
Expand All @@ -13,7 +19,6 @@ <h2>Message Attributes</h2>
</div>
</div>
}
}

<div class="new-attribute-input">
<mat-form-field appearance="outline">
Expand All @@ -25,10 +30,20 @@ <h2>Message Attributes</h2>
</mat-form-field>
</div>
<button mat-stroked-button id="add-button" (click)="addAttribute()">Add Attribute <mat-icon>add</mat-icon></button>
} @else {
<mat-form-field appearance="outline" class="json-field">
<textarea matInput [formControl]="jsonInput" placeholder='{"key": "value"}'></textarea>
</mat-form-field>
@if(jsonError) {
<div class="json-error">Invalid JSON — must be an object with string values, e.g.
<code>{{ '{' }}"key": "value"{{ '}' }}</code></div>
}
}

</mat-dialog-content>

<mat-dialog-actions class="split-view">
<button mat-flat-button (click)="discardChanges()">Cancel</button>
<button mat-stroked-button color="warn" (click)="clearAll()">Clear All <mat-icon>clear_all</mat-icon></button>
<button mat-raised-button color="primary" (click)="saveChanges()">Save Changes</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,24 @@
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 8px;

.key {
flex-shrink: 0;
width: 40%;
}

.value {
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

.actions {
flex-shrink: 0;
}
}

.new-attribute-input {
Expand All @@ -24,4 +40,35 @@
align-self: center;
text-align: center;
width: 100%;
}

.dialog-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 16px;
}

.json-field {
width: 100%;

textarea {
min-height: 160px;
font-family: monospace;
font-size: 13px;
}
}

.json-error {
color: var(--mdc-theme-error, #f44336);
font-size: 12px;
margin-top: -8px;
margin-bottom: 8px;

code {
background: rgba(0, 0, 0, 0.06);
padding: 1px 4px;
border-radius: 3px;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ import { MatFormField, MatInput } from '@angular/material/input';
styleUrl: './attribute-editor.component.scss'
})
export class AttributeEditorComponent {
public attributes: { [key: string]: string } = inject(MAT_DIALOG_DATA).attributes
public attributes: { [key: string]: string } = { ...inject(MAT_DIALOG_DATA).attributes }
newKeyControl = new FormControl<string>("", Validators.required)
newValueControl = new FormControl<string>("", Validators.required)
jsonMode = false
jsonInput = new FormControl<string>("")
jsonError = false

constructor(
private dialogRef: MatDialogRef<AttributeEditorComponent>
Expand All @@ -49,11 +52,55 @@ export class AttributeEditorComponent {
}
}

toggleJsonMode() {
if (!this.jsonMode) {
// Entering JSON mode: serialize current attributes
this.jsonInput.setValue(JSON.stringify(this.attributes, null, 2))
this.jsonError = false
this.jsonMode = true
} else {
// Leaving JSON mode: try to parse
if (this.applyJson()) {
this.jsonMode = false
}
}
}

private applyJson(): boolean {
try {
const parsed = JSON.parse(this.jsonInput.value ?? '{}')
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
this.jsonError = true
return false
}
for (const val of Object.values(parsed)) {
if (typeof val !== 'string') {
this.jsonError = true
return false
}
}
this.attributes = parsed
this.jsonError = false
return true
} catch {
this.jsonError = true
return false
}
}

clearAll() {
this.attributes = {}
this.jsonInput.setValue('{}')
}

discardChanges() {
this.dialogRef.close()
}

saveChanges() {
if (this.jsonMode) {
if (!this.applyJson()) return
}
this.dialogRef.close(this.attributes)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ <h3>Publish Message to topic: {{this.topic?.name}}</h3>
<button mat-stroked-button color="accent" (click)="editAttributes()">
Edit Attributes ({{this.attributeCount}})
</button>
@if(this.attributeCount > 0) {
<button mat-stroked-button color="warn" (click)="clearAttributes()">
Clear Attributes
<mat-icon>clear</mat-icon>
</button>
}
<button mat-raised-button color="primary" [disabled]="this.inputField.invalid" (click)="this.publishMessage()">
Publish Message!
<mat-icon>send</mat-icon>
Expand Down
44 changes: 37 additions & 7 deletions webapp/src/app/components/topic-details/topic-details.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { Component, EventEmitter, inject, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { ReactiveFormsModule, UntypedFormControl, Validators } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
Expand All @@ -9,14 +9,18 @@ import { MatInput } from '@angular/material/input';
import { Topic } from 'src/app/services/pubsub.service';
import { AttributeEditorComponent } from './attribute-editor/attribute-editor.component';

function attributesStorageKey(topicName: string): string {
return `message-attributes:${topicName}`
}

@Component({
selector: 'app-topic-details',
templateUrl: './topic-details.component.html',
styleUrls: ['./topic-details.component.scss'],
standalone: true,
imports: [MatButton, MatIcon, MatFormField, MatInput, CdkTextareaAutosize, ReactiveFormsModule]
})
export class TopicDetailsComponent implements OnInit {
export class TopicDetailsComponent implements OnInit, OnChanges {

@Input() topic?: Topic
@Output() onMessagePublish = new EventEmitter<{ topic: Topic, message: string, attributes: { [key: string]: string } }>()
Expand All @@ -28,26 +32,52 @@ export class TopicDetailsComponent implements OnInit {
constructor() { }

ngOnInit(): void {
this.loadAttributes()
}

ngOnChanges(changes: SimpleChanges): void {
if (changes['topic'] && !changes['topic'].firstChange) {
this.loadAttributes()
}
}

private loadAttributes(): void {
this.attributes = {}
this.attributeCount = 0
if (!this.topic) return
const stored = localStorage.getItem(attributesStorageKey(this.topic.name))
if (stored) {
try {
this.attributes = JSON.parse(stored)
this.attributeCount = Object.keys(this.attributes).length
} catch (e) {
console.error('Failed to parse stored attributes for topic', this.topic?.name, e)
}
}
}

editAttributes() {
let dialogRef = this._dialog.open(AttributeEditorComponent, { data: { attributes: this.attributes } })
let dialogRef = this._dialog.open(AttributeEditorComponent, { data: { attributes: { ...this.attributes } } })

dialogRef.afterClosed().subscribe(result => {
if (result) {
if (result !== undefined) {
this.attributes = result
this.attributeCount = Object.keys(this.attributes).length
localStorage.setItem(attributesStorageKey(this.topic!.name), JSON.stringify(this.attributes))
}
})
}

clearAttributes() {
this.attributes = {}
this.attributeCount = 0
localStorage.removeItem(attributesStorageKey(this.topic!.name))
}

publishMessage() {
console.log("this value was found", this.inputField.value)

this.onMessagePublish.emit({ topic: this.topic!, message: this.inputField.value, attributes: this.attributes })
this.inputField.reset()
this.attributes = {}
this.attributeCount = 0
}

}