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
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ import { NewTrackDialogComponent } from './components/new-track-dialog/new-track
import { ReleaseTrackPageComponent } from './views/dashboard-page/release-management/release-track-page/release-track-page.component';
import { StatusChipComponent } from './components/status-chip/status-chip.component';
import { WorkbenchChipComponent } from './components/workbench-chip/workbench-chip.component';
import { BulkPromoteDialogComponent } from './components/bulk-update-dialog/bulk-update-dialog.component';

export function initConfig(appConfigService: AppConfigService) {
return () => appConfigService.loadAppConfig();
Expand All @@ -267,6 +268,7 @@ export function initConfig(appConfigService: AppConfigService) {
SaveDialogComponent,
WorkflowStatusDialogComponent,
AddDialogComponent,
BulkPromoteDialogComponent,
DeleteDialogComponent,
HistoryTimelineComponent,
MembershipSectionComponent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<div class="add-dialog">
<h1 *ngIf="config.title" mat-dialog-title>{{ config.title }}</h1>
<mat-dialog-content>
<div *ngIf="hasSelectableContent; else nothing">
<app-stix-list
*ngIf="config.type !== 'user'"
[config]="
config.stixListConfig || {
select: config.selectionType
? config.selectionType
: config.select
? 'many'
: 'disabled',
selectionModel: config.select,
type: config.type,
clickBehavior: 'expand',
stixObjects: config.selectableObjects
? config.selectableObjects
: undefined,
showFilters: false,
}
"></app-stix-list>
</div>
<ng-template #nothing>
<h3>No available objects.</h3>
</ng-template>
</mat-dialog-content>
<mat-dialog-actions align="center">
<ng-container *ngIf="hasSelectableContent; else closeDialog">
<button
*ngIf="config.selectAll"
mat-raised-button
class="extended-button left-button"
color="primary"
(click)="selectAll()">
<span class="text-label">{{ 'Select All' }}</span>
</button>
<button
*ngIf="config.canPromote"
mat-raised-button
class="extended-button left-button"
color="primary"
(click)="dialogRef.close(true)">
<span class="text-label"> Promote Selection </span>
</button>
<button
*ngIf="config.canDemote"
mat-raised-button
class="extended-button left-button"
color="primary"
(click)="dialogRef.close(true)">
<span class="text-label"> Demote Selection </span>
</button>
<button
*ngIf="config.clearSelection"
mat-raised-button
class="extended-button"
color="primary"
(click)="clearSelections()">
<span class="text-label">Clear</span>
</button>
<button
mat-raised-button
class="extended-button right-button"
color="primary"
(click)="dialogRef.close()">
<span class="text-label">Cancel</span>
</button>
</ng-container>
<ng-template #closeDialog>
<button class="extended-button" mat-raised-button mat-dialog-close>
close
</button>
</ng-template>
</mat-dialog-actions>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@use '../../../style/globals';
@use '../../../style/colors';

.add-dialog {
.mat-mdc-list-item:hover h3 {
color: colors.color(primary);
}
.mat-mdc-list-item h3 {
@extend .subheading;
font-weight: 700 !important;
}
.mat-mdc-list-item + .mat-list-item {
.dark & {
border-top: 1px solid colors.border-color(dark);
}
.light & {
border-top: 1px solid colors.border-color(light);
}
}

min-width: 40vw;
max-width: 80vw;
max-height: 75vh;
padding: 24px;

.left-button {
margin-right: 12px;
}

.right-button {
margin-left: 12px;
}

.mat-mdc-checkbox {
margin-bottom: 16px;
display: block;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Component, Inject, ViewEncapsulation } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { StixObject } from 'src/app/classes/stix/stix-object';
import { SelectionModel } from '@angular/cdk/collections';
import { StixListConfig } from '../stix/stix-list/stix-list.component';

@Component({
selector: 'app-bulk-update-dialog',
templateUrl: './bulk-update-dialog.component.html',
styleUrls: ['./bulk-update-dialog.component.scss'],
encapsulation: ViewEncapsulation.None,
standalone: false,
})
export class BulkPromoteDialogComponent {
constructor(
public dialogRef: MatDialogRef<BulkPromoteDialogComponent>,
@Inject(MAT_DIALOG_DATA) public config: BulkPromoteDialogConfig
) {}

public get stixObjects(): StixObject[] {
if (Array.isArray(this.config.stixListConfig?.stixObjects)) {
return this.config.stixListConfig.stixObjects;
}
return this.config.selectableObjects || [];
}

public get hasSelectableContent(): boolean {
return (
this.config.type === 'user' ||
this.config.type === 'analytic' ||
!!this.config.stixListConfig ||
(this.config.selectableObjects?.length ?? 0) > 0
);
}

public clearSelections() {
this.config.select.clear();
this.dialogRef.close(true);
}

public selectAll(): void {
const ids = this.stixObjects.map(obj => obj.stixID);
this.config.select.select(...ids);
}
}

export interface BulkPromoteDialogConfig {
selectableObjects?: StixObject[]; // Stix Object array of selectable objects not in list
type: string; // type to display stix list
select: SelectionModel<string>; // selection model to retrieve list of selected object
selectAll?: boolean;
selectionType?: 'many' | 'one' | 'disabled'; // defaults to 'many' if a selection model is given
canPromote?: boolean;
canDemote?: boolean;
title?: string; // dialog text
clearSelection?: boolean; //boolean to add clear selection button
stixListConfig?: StixListConfig; // optional full stix-list config
}
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,22 @@ <h3>Resolution Details</h3>
<mat-icon>add</mat-icon>
Add
</button>
<button
*ngIf="canBulkPromoteStatus(lane)"
class="view-color"
mat-stroked-button
(click)="onBulkUpdate(true)">
<mat-icon>edit</mat-icon>
Edit
</button>
<button
*ngIf="canBulkDemoteStatus(lane)"
class="view-color"
mat-stroked-button
(click)="onBulkUpdate(false)">
<mat-icon>edit</mat-icon>
Edit
</button>
<button
*ngIf="lane.isReleasedMembers"
mat-stroked-button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
ReleaseTrackSnapshotOptions,
ReleaseTrackType,
ResolutionStrategy,
ReviewPayload,
SnapshotScheduleMode,
SnapshotScheduleModeType,
SnapshotTier,
Expand All @@ -37,6 +38,7 @@ import {
import { StixObject } from 'src/app/classes/stix';
import { AddDialogComponent } from 'src/app/components/add-dialog/add-dialog.component';
import { ConfirmationDialogComponent } from 'src/app/components/confirmation-dialog/confirmation-dialog.component';
import { BulkPromoteDialogComponent } from 'src/app/components/bulk-update-dialog/bulk-update-dialog.component';
import { DeleteDialogComponent } from 'src/app/components/delete-dialog/delete-dialog.component';
import { MultipleChoiceDialogComponent } from 'src/app/components/multiple-choice-dialog/multiple-choice-dialog.component';
import {
Expand Down Expand Up @@ -804,6 +806,7 @@ export class ReleaseTrackPageComponent implements OnInit {
select: selection,
type: 'all',
selectionType: 'many',
selectAll: true,
buttonLabel: 'Add',
title: 'Add candidates',
clearSelection: true,
Expand Down Expand Up @@ -846,6 +849,71 @@ export class ReleaseTrackPageComponent implements OnInit {
return attackType ? `/${attackType}/${stixId}` : null;
}

public onBulkUpdate(toStaged: boolean): void {
if (!this.id || !this.releaseTrack) return;
const laneList = toStaged ? this.candidates : this.staged;

const candidateIds = laneList
.map(item => ({
stixID: item.object_ref,
attackID: item.attack_id,
modified: item.object_modified,
attackType: this.formatStixType(item.object_ref),
...item,
}))
.filter((item: any) =>
toStaged
? this.getObjectStatus(item) === WorkflowStatus.AwaitingReview
: this.getObjectStatus(item) === WorkflowStatus.Reviewed
);

const selection = new SelectionModel<string>(true);
const dialogRef = this.dialog.open(BulkPromoteDialogComponent, {
data: {
select: selection,
selectAll: true,
type: 'all',
selectionType: 'many',
canPromote: toStaged ? true : false,
canDemote: toStaged ? false : true,
title: 'Select candidates',
clearSelection: true,
stixListConfig: {
stixObjects: candidateIds,
select: 'many',
selectionModel: selection,
},
},
maxWidth: '90vw',
width: '70vw',
maxHeight: '85vh',
});

dialogRef.afterClosed().subscribe({
next: result => {
if (!result || !selection.selected.length) return;

const body: ReviewPayload = {
from: toStaged
? WorkflowStatus.AwaitingReview
: WorkflowStatus.Reviewed,
to: toStaged
? WorkflowStatus.Reviewed
: WorkflowStatus.AwaitingReview,
object_refs: selection.selected,
};
this.connector.reviewCandidates(this.id, body).subscribe({
next: () => {
this.refreshReleaseTrackState();
},
error: err => {
console.error('Failed to change status', err);
},
});
},
});
}

public promote(objectIds: string[]): void {
if (!objectIds.length) return;
const sub = this.connector.promoteCandidates(this.id, objectIds).subscribe({
Expand Down Expand Up @@ -1006,6 +1074,14 @@ export class ReleaseTrackPageComponent implements OnInit {
);
}

public canBulkDemoteStatus(lane: ReleaseTrackWorkspaceLane): boolean {
return lane.key === 'staged';
}

public canBulkPromoteStatus(lane: ReleaseTrackWorkspaceLane): boolean {
return lane.key === 'candidates-awaiting-review';
}

public canManuallyPromote(lane: ReleaseTrackWorkspaceLane): boolean {
return !this.autoPromotionEnabled && lane.type === 'candidate';
}
Expand Down
Loading