Merge pull request 'Begin new alert rule builder' (#100) from alertRuleBuilder91 into trunk

Reviewed-on: #100
This commit is contained in:
amigan 2025-01-11 16:47:06 -05:00
commit 66dc6e4b78
12 changed files with 90 additions and 46 deletions

View file

@ -169,7 +169,11 @@
</td> </td>
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr> <tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: columns" [ngClass]="{'in-incident': row.incidents > 0}"></tr> <tr
mat-row
*matRowDef="let row; columns: columns"
[ngClass]="{ 'in-incident': row.incidents > 0 }"
></tr>
</table> </table>
</div> </div>
<div class="pagFoot"> <div class="pagFoot">

View file

@ -1,7 +1,12 @@
@let inc = inc$ | async; @let inc = inc$ | async;
<mat-card class="incident" appearance="outlined"> <mat-card class="incident" appearance="outlined">
<div class="cardHdr"> <div class="cardHdr">
<h1>{{ inc?.name }}</h1> <h1>
{{ inc?.name }}
<a [href]="'/api/incident/' + incID + '.m3u'"
><mat-icon>playlist_play</mat-icon></a
>
</h1>
<button mat-icon-button (click)="editIncident(incID)"> <button mat-icon-button (click)="editIncident(incID)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>

View file

@ -15,12 +15,19 @@ export class AlertTime {
} }
export class AlertRule { export class AlertRule {
times: AlertTime[]; times!: string[];
mult: number; mult!: number;
constructor(times: AlertTime[], mult: number) { public getTimes(): AlertTime[] {
this.times = times; let timesProc = <AlertTime[]>[];
this.mult = mult; this.times.forEach((tm) => {
let sr = tm.split('+');
timesProc.push(<AlertTime>{
time: sr[0],
duration: sr[1],
});
});
return timesProc;
} }
} }
@ -84,6 +91,10 @@ export class Talkgroup {
icon?: string, icon?: string,
) { ) {
this.iconSvg = this.iconMap(this.metadata?.icon!); this.iconSvg = this.iconMap(this.metadata?.icon!);
this.alert_rules = this.alert_rules.map((x) =>
Object.assign(new AlertRule(), x),
);
console.log(this.alert_rules);
} }
iconMap(icon: string): string { iconMap(icon: string): string {

View file

@ -1,30 +1,21 @@
<div class="container flex"> <div class="container flex">
@for (rule of rules; track $index) { @for (rule of rules; track $index) {
<div class=""> <div class="rule">
<table class="table"> <table mat-table [dataSource]="rule | ruleTimes">
<thead> <ng-container matColumnDef="time">
<tr> <th mat-header-cell *matHeaderCellDef>Time</th>
<td>Start</td> <td mat-cell *matCellDef="let time">{{ time.time }}</td>
<td>Duration</td> </ng-container>
<td></td> <ng-container matColumnDef="duration">
</tr> <th mat-header-cell *matHeaderCellDef>Duration</th>
</thead> <td mat-cell *matCellDef="let time">{{ time.duration }}</td>
<tbody> </ng-container>
@for (time of rule.times; track $index) { <ng-container matColumnDef="multiplier">
<tr> <th mat-header-cell *matHeaderCellDef>Multiplier</th>
<td> <td mat-cell *matCellDef="let time">{{ rule.mult }}</td>
{{ time.time }} </ng-container>
</td> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<td> <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
{{ time.duration }}
</td>
</tr>
} @empty {
<tr>
<td><em>No times</em></td>
</tr>
}
</tbody>
</table> </table>
</div> </div>
} @empty { } @empty {

View file

@ -1,18 +1,43 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'; import {
import { AlertRule } from '../../../talkgroup'; Component,
Input,
Output,
EventEmitter,
Pipe,
PipeTransform,
} from '@angular/core';
import { AlertRule, AlertTime } from '../../../talkgroup';
import { MatTableModule } from '@angular/material/table';
@Pipe({
name: 'ruleTimes',
standalone: true,
pure: true,
})
export class AlertRulePipe implements PipeTransform {
transform(rule: AlertRule, args?: any): AlertTime[] {
return rule.getTimes();
}
}
@Component({ @Component({
selector: 'alert-rule-builder', selector: 'alert-rule-builder',
imports: [], imports: [MatTableModule, AlertRulePipe],
templateUrl: './alert-rule-builder.component.html', templateUrl: './alert-rule-builder.component.html',
styleUrl: './alert-rule-builder.component.scss', styleUrl: './alert-rule-builder.component.scss',
}) })
export class AlertRuleBuilderComponent { export class AlertRuleBuilderComponent {
@Input() rules: AlertRule[] = []; @Input() rules!: AlertRule[];
@Output() rulesChange: EventEmitter<AlertRule[]> = new EventEmitter< @Output() rulesChange: EventEmitter<AlertRule[]> = new EventEmitter<
AlertRule[] AlertRule[]
>(); >();
displayedColumns = ['time', 'duration', 'multiplier'];
ngOnInit() {
this.rules = <AlertRule[]>this.rules;
}
emit() { emit() {
this.rulesChange.emit(this.rules); this.rulesChange.emit(this.rules);
} }

View file

@ -7,6 +7,7 @@ import {
IconMap, IconMap,
iconMapping, iconMapping,
TGID, TGID,
AlertRule,
} from '../../talkgroup'; } from '../../talkgroup';
import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { TalkgroupService } from '../talkgroups.service'; import { TalkgroupService } from '../talkgroups.service';
@ -157,6 +158,10 @@ export class TalkgroupRecordComponent {
.getTalkgroup(Number(this.tgid.sys), Number(this.tgid.tg)) .getTalkgroup(Number(this.tgid.sys), Number(this.tgid.tg))
.pipe( .pipe(
tap((tg) => { tap((tg) => {
console.log('tap run');
tg.alert_rules = tg.alert_rules
? tg.alert_rules.map((x) => Object.assign(new AlertRule(), x))
: [];
this.form.patchValue(tg); this.form.patchValue(tg);
this.form.controls['tagInput'].setValue(''); this.form.controls['tagInput'].setValue('');
this.form.controls['tagsControl'].setValue(this.tg?.tags ?? []); this.form.controls['tagsControl'].setValue(this.tg?.tags ?? []);

View file

@ -33,7 +33,10 @@ export class TalkgroupService {
private subscriptions = new Subscription(); private subscriptions = new Subscription();
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
this.tgs$ = this.fetchAll.pipe(switchMap(() => this.getTalkgroups())); this.tgs$ = this.fetchAll.pipe(switchMap(() => this.getTalkgroups()));
this.tags$ = this.fetchAll.pipe(switchMap(() => this.getAllTags())); this.tags$ = this.fetchAll.pipe(
switchMap(() => this.getAllTags()),
shareReplay(),
);
this.fillTgMap(); this.fillTgMap();
} }
@ -52,11 +55,8 @@ export class TalkgroupService {
getTalkgroup(sys: number, tg: number): Observable<Talkgroup> { getTalkgroup(sys: number, tg: number): Observable<Talkgroup> {
const key = this.tgKey(sys, tg); const key = this.tgKey(sys, tg);
if (!this._getTalkgroup.get(key)) { if (!this._getTalkgroup.get(key)) {
return this.tgs$.pipe( let rs = new ReplaySubject<Talkgroup>();
switchMap((talkg) => this._getTalkgroup.set(key, rs);
talkg.filter((tgv) => tgv.tgid == tg && tgv.system_id == sys),
),
);
} }
return this._getTalkgroup.get(key)!; return this._getTalkgroup.get(key)!;
} }

View file

@ -245,6 +245,7 @@ func (a *Auth) routeAuth(w http.ResponseWriter, r *http.Request) {
Path: "/", Path: "/",
HttpOnly: true, HttpOnly: true,
Secure: true, Secure: true,
MaxAge: 60 * 60 * 24 * 30, // one month
} }
cookie.Domain = r.Host cookie.Domain = r.Host

View file

@ -180,6 +180,7 @@ FROM incidents_calls ic, LATERAL (
FROM swept_calls sc WHERE sc.id = ic.swept_call_id FROM swept_calls sc WHERE sc.id = ic.swept_call_id
) c ) c
WHERE ic.incident_id = $1 WHERE ic.incident_id = $1
ORDER BY ic.call_date ASC
` `
type GetIncidentCallsRow struct { type GetIncidentCallsRow struct {

View file

@ -202,7 +202,7 @@ func (ia *incidentsAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) {
return return
} }
var b bytes.Buffer b := new(bytes.Buffer)
callUrl := common.PtrTo(*ia.baseURL) callUrl := common.PtrTo(*ia.baseURL)
@ -220,7 +220,7 @@ func (ia *incidentsAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) {
callUrl.Path = "/api/call/" + c.ID.String() callUrl.Path = "/api/call/" + c.ID.String()
fmt.Fprintf(w, "#EXTINF:%d,%s%s (%s)\n%s\n\n", fmt.Fprintf(b, "#EXTINF:%d,%s%s (%s)\n%s\n\n",
c.Duration.Seconds(), c.Duration.Seconds(),
tg.StringTag(true), tg.StringTag(true),
from, from,

View file

@ -141,7 +141,8 @@ FROM incidents_calls ic, LATERAL (
sc.transcript sc.transcript
FROM swept_calls sc WHERE sc.id = ic.swept_call_id FROM swept_calls sc WHERE sc.id = ic.swept_call_id
) c ) c
WHERE ic.incident_id = @id; WHERE ic.incident_id = @id
ORDER BY ic.call_date ASC;
-- name: GetIncident :one -- name: GetIncident :one
SELECT SELECT