Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@angular/forms": "^19.2.25",
"@angular/material": "^19.2.16",
"@angular/material-moment-adapter": "^19.2.16",
"@mitre-attack/attack-data-model": "^5.0.0-beta.1",
"@mitre-attack/attack-data-model": "^5.0.0-beta.3",
"@angular/platform-browser": "^19.2.25",
"@angular/platform-browser-dynamic": "^19.2.25",
"@angular/router": "^19.2.25",
Expand Down
26 changes: 22 additions & 4 deletions src/app/classes/release-tracks/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Composition, CompositionResolution } from './composition';
import { ReleaseTrackConfig } from './config';
import { ReleaseTrackType } from './enums';
import { VersionHistoryEntry } from './history';
import { SnapshotSchedule } from './release-track';
import {
CandidateEntry,
MemberEntry,
Expand Down Expand Up @@ -34,6 +35,7 @@ export class ReleaseTrackSnapshot {
// virtual track composition
public composition?: Composition;
public composition_resolution?: CompositionResolution;
public snapshot_schedule?: SnapshotSchedule;

constructor(raw?: any) {
if (raw) this.deserialize(raw);
Expand Down Expand Up @@ -105,15 +107,21 @@ export class ReleaseTrackSnapshot {

if ('members' in raw && Array.isArray(raw.members)) {
this.members = raw.members.map((m: any) => ({
...m,
object_ref: m.object_ref,
object_modified: new Date(m.object_modified),
object_modified: m.object_modified
? new Date(m.object_modified)
: undefined,
}));
}

if ('staged' in raw && Array.isArray(raw.staged)) {
this.staged = raw.staged.map((s: any) => ({
...s,
object_ref: s.object_ref,
object_modified: new Date(s.object_modified),
object_modified: s.object_modified
? new Date(s.object_modified)
: undefined,
object_status: s.object_status,
object_staged_at: s.object_staged_at
? new Date(s.object_staged_at)
Expand All @@ -124,8 +132,11 @@ export class ReleaseTrackSnapshot {

if ('candidates' in raw && Array.isArray(raw.candidates)) {
this.candidates = raw.candidates.map((c: any) => ({
...c,
object_ref: c.object_ref,
object_modified: new Date(c.object_modified),
object_modified: c.object_modified
? new Date(c.object_modified)
: undefined,
object_status: c.object_status,
object_added_at: c.object_added_at
? new Date(c.object_added_at)
Expand All @@ -136,8 +147,11 @@ export class ReleaseTrackSnapshot {

if ('quarantine' in raw && Array.isArray(raw.quarantine)) {
this.quarantine = raw.quarantine.map((q: any) => ({
...q,
object_ref: q.object_ref,
object_modified: new Date(q.object_modified),
object_modified: q.object_modified
? new Date(q.object_modified)
: undefined,
source_track_id: q.source_track_id,
source_track_name: q.source_track_name,
source_snapshot_version: q.source_snapshot_version,
Expand All @@ -147,6 +161,9 @@ export class ReleaseTrackSnapshot {

if ('composition' in raw) this.composition = raw.composition;

if ('snapshot_schedule' in raw)
this.snapshot_schedule = raw.snapshot_schedule;

if ('composition_resolution' in raw && raw.composition_resolution) {
const cr = raw.composition_resolution;
const resolved: CompositionResolution = { ...cr } as any;
Expand Down Expand Up @@ -219,6 +236,7 @@ export class ReleaseTrackSnapshot {
})),
composition: this.composition,
composition_resolution: this.composition_resolution,
snapshot_schedule: this.snapshot_schedule,
};
}

Expand Down
162 changes: 129 additions & 33 deletions src/app/components/new-track-dialog/new-track-dialog.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,63 +84,159 @@ <h2>

<!-- Virtual track -->
<div *ngIf="isVirtual">
<div class="section-block">
<div class="section-title">Object Deduplication</div>
<div class="section-block stack">
<div class="section-title">Component Tracks</div>
<div class="section-sub">
Configure deduplication rules for the virtual track
Standard tracks to include in this virtual release track.
</div>
</div>

<div formGroupName="composition" class="composition-structured">
<mat-form-field appearance="outline" class="section-block">
<mat-label>Strategy</mat-label>
<mat-select formControlName="deduplicationStrategy">
<mat-option
*ngFor="let opt of deduplicationOptions"
[value]="opt"
>{{ formatOption(opt) }}</mat-option
>
</mat-select>
<mat-hint
>How conflicting objects from component tracks are
resolved</mat-hint
>
</mat-form-field>
<div *ngIf="isLoadingComponentTracks" class="loading-row">
<mat-spinner diameter="24"></mat-spinner>
<span>Loading component tracks</span>
</div>

<div
*ngIf="!isLoadingComponentTracks && !componentTrackOptions.length"
class="empty-state">
No standard release tracks found.
</div>

<div class="section-block gap-12">
<div
*ngIf="componentTrackOptions.length"
class="component-track-picker">
<mat-form-field appearance="outline">
<mat-label>Snapshot Tier</mat-label>
<mat-select formControlName="deduplicationTier">
<mat-label>Add component track</mat-label>
<input
matInput
type="text"
formControlName="componentTrackSearch"
[matAutocomplete]="componentTrackAutocomplete"
placeholder="Search standard tracks" />
<mat-icon matSuffix>arrow_drop_down</mat-icon>
<mat-autocomplete
#componentTrackAutocomplete="matAutocomplete"
[displayWith]="displayComponentTrack"
(optionSelected)="selectComponentTrack($event)">
<mat-option
*ngFor="let opt of deduplicationTierOptions"
[value]="opt"
>{{ formatOption(opt) }}</mat-option
>
</mat-select>
*ngFor="let track of filteredComponentTrackOptions"
[value]="track">
<span class="component-track-option-label">
<span class="component-track-option-name">
{{ track.name }}
</span>
<span class="component-track-option-meta">
({{ getComponentTrackSnapshotLabel(track) }})
</span>
</span>
</mat-option>
<mat-option
*ngIf="!filteredComponentTrackOptions.length"
disabled>
No matching standard tracks
</mat-option>
</mat-autocomplete>
<mat-hint>
Prefer objects from this tier when resolving conflicts
Select one or more standard tracks to include in this virtual
track.
</mat-hint>
</mat-form-field>
</div>

<div
*ngIf="selectedComponentTracks.length"
class="selected-component-track-list">
<div
*ngFor="let track of selectedComponentTracks"
class="component-track-option selected">
<div class="component-track-selected-header">
<div>
<div class="component-track-name">{{ track.name }}</div>
<div class="component-track-meta">
{{ getComponentTrackSnapshotLabel(track) }}
</div>
</div>
<button
mat-icon-button
type="button"
aria-label="Remove component track"
(click)="removeComponentTrack(track)">
<mat-icon>close</mat-icon>
</button>
</div>

<p *ngIf="track.description" class="component-track-description">
{{ track.description }}
</p>

<mat-form-field
*ngIf="track.selected"
appearance="outline"
class="object-type-filter">
<mat-label>Object type filter</mat-label>
<mat-select multiple [(value)]="track.objectTypes">
<mat-option
*ngFor="let opt of objectTypeOptions"
[value]="opt.value">
{{ opt.label }}
</mat-option>
</mat-select>
<mat-hint>Leave empty to include all object types</mat-hint>
</mat-form-field>
</div>
</div>

<mat-divider style="margin: 24px 0 16px"></mat-divider>

<div class="section-block stack">
<div class="section-title">Object Deduplication</div>
<mat-form-field appearance="outline">
<mat-label>Workflow Status</mat-label>
<mat-select formControlName="deduplicationStatus">
<mat-label>Strategy</mat-label>
<mat-select formControlName="deduplicationStrategy">
<mat-option
*ngFor="let opt of deduplicationStatusOptions"
*ngFor="let opt of deduplicationOptions"
[value]="opt"
>{{ formatOption(opt) }}</mat-option
>
</mat-select>
<mat-hint>
Prefer objects with this status when resolving conflicts
</mat-hint>
</mat-form-field>

<div class="section-block gap-12">
<mat-form-field appearance="outline">
<mat-label>Snapshot Tier</mat-label>
<mat-select formControlName="deduplicationTier">
<mat-option
*ngFor="let opt of deduplicationTierOptions"
[value]="opt"
>{{ formatOption(opt) }}</mat-option
>
</mat-select>
<mat-hint>
Prefer objects from this tier when resolving conflicts
</mat-hint>
</mat-form-field>

<mat-form-field appearance="outline">
<mat-label>Workflow Status</mat-label>
<mat-select formControlName="deduplicationStatus">
<mat-option
*ngFor="let opt of deduplicationStatusOptions"
[value]="opt"
>{{ formatOption(opt) }}</mat-option
>
</mat-select>
<mat-hint>
Prefer objects with this status when resolving conflicts
</mat-hint>
</mat-form-field>
</div>
</div>
</div>

<mat-divider style="margin: 12px 0"></mat-divider>

<div class="section-block">
<div class="section-block stack">
<div class="section-title">Snapshot Schedule</div>
<div class="section-sub">
Configure automatic snapshot scheduling for this virtual track
Expand Down
Loading
Loading