Skip to content

Commit e42bcd2

Browse files
committed
Loader: Import legace DSOMM progress from localStorage; Improved error handling
1 parent b08900f commit e42bcd2

File tree

10 files changed

+167
-87
lines changed

10 files changed

+167
-87
lines changed

src/app/component/modal-message/modal-message.component.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
MatDialogConfig,
77
} from '@angular/material/dialog';
88
import * as md from 'markdown-it';
9+
import { MarkdownText } from 'src/app/model/markdown-text';
10+
import { NotificationService } from 'src/app/service/notification.service';
911

1012
@Component({
1113
selector: 'app-modal-message',
@@ -32,13 +34,18 @@ export class ModalMessageComponent implements OnInit {
3234
constructor(
3335
public dialog: MatDialog,
3436
public dialogRef: MatDialogRef<ModalMessageComponent>,
35-
@Inject(MAT_DIALOG_DATA) data: DialogInfo
37+
@Inject(MAT_DIALOG_DATA) data: DialogInfo,
38+
private notificationService: NotificationService
3639
) {
3740
this.data = data;
3841
}
3942

4043
// eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method
41-
ngOnInit(): void {}
44+
ngOnInit(): void {
45+
this.notificationService.message$.subscribe(({ title, message }) => {
46+
this.openDialog(new DialogInfo(message, title));
47+
});
48+
}
4249

4350
openDialog(dialogInfo: DialogInfo | string): MatDialogRef<ModalMessageComponent> {
4451
// Remove focus from the button that becomes aria unavailable (avoids ugly console error message)
@@ -77,7 +84,8 @@ export class DialogInfo {
7784
buttons: string[] = ['OK'];
7885

7986
constructor(msg: string = '', title: string = '') {
80-
this.message = msg;
87+
let md: MarkdownText = new MarkdownText(msg);
88+
this.message = md.render();
8189
this.title = title;
8290
}
8391
}

src/app/model/progress-store.ts

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -425,30 +425,35 @@ export class ProgressStore {
425425
try {
426426
const legacyDataset = JSON.parse(stored);
427427

428-
legacyDataset.forEach((entry: any) => {
429-
const legacyActivities = entry.Activity;
430-
431-
if (Array.isArray(legacyActivities)) {
432-
legacyActivities.forEach((legacyActivity: any) => {
433-
const activityUuid = legacyActivity.uuid;
434-
const teamsImplemented = legacyActivity.teamsImplemented;
435-
if (teamsImplemented) {
436-
Object.keys(teamsImplemented).forEach((team: string) => {
437-
if (legacyActivity.teamsImplemented[team]) {
438-
if (!progress![activityUuid]) {
439-
progress![activityUuid] = {};
428+
try {
429+
legacyDataset.forEach((entry: any) => {
430+
const legacyActivities = entry.Activity;
431+
432+
if (Array.isArray(legacyActivities)) {
433+
legacyActivities.forEach((legacyActivity: any) => {
434+
const activityUuid = legacyActivity.uuid;
435+
const teamsImplemented = legacyActivity.teamsImplemented;
436+
if (teamsImplemented) {
437+
Object.keys(teamsImplemented).forEach((team: string) => {
438+
if (legacyActivity.teamsImplemented[team]) {
439+
if (!progress![activityUuid]) {
440+
progress![activityUuid] = {};
441+
}
442+
if (!progress![activityUuid][team]) {
443+
progress![activityUuid][team] = {};
444+
}
445+
console.log(`Legacy import: Setting '${completeTitle}' on ${activityUuid} for ${team}`); // eslint-disable-line
446+
progress![activityUuid][team][completeTitle] = new Date();
440447
}
441-
if (!progress![activityUuid][team]) {
442-
progress![activityUuid][team] = {};
443-
}
444-
console.log(`Legacy import: Setting '${completeTitle}' on ${activityUuid} for ${team}`); // eslint-disable-line
445-
progress![activityUuid][team][completeTitle] = new Date();
446-
}
447-
});
448-
}
449-
});
450-
}
451-
});
448+
});
449+
}
450+
});
451+
}
452+
});
453+
} catch (error) {
454+
console.error('Failed to process legacy dataset activities: ', error);
455+
throw Error('Unexpected data structure: ' + error);
456+
}
452457
} catch (error) {
453458
console.error('Failed to parse legacy progress: ', error);
454459
throw Error(

src/app/pages/matrix/matrix.component.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@ import { Activity, ActivityStore, Data } from 'src/app/model/activity-store';
77
import { UntilDestroy } from '@ngneat/until-destroy';
88
import { MatChip, MatChipList } from '@angular/material/chips';
99
import { deepCopy } from 'src/app/util/util';
10-
import {
11-
ModalMessageComponent,
12-
DialogInfo,
13-
} from '../../component/modal-message/modal-message.component';
1410
import { DataStore } from 'src/app/model/data-store';
1511
import { perfNow } from 'src/app/util/util';
1612
import { SettingsService } from 'src/app/service/settings/settings.service';
13+
import { NotificationService } from 'src/app/service/notification.service';
1714

1815
export interface MatrixRow {
1916
Category: string;
@@ -52,7 +49,7 @@ export class MatrixComponent implements OnInit {
5249
private loader: LoaderService,
5350
private settings: SettingsService,
5451
private router: Router,
55-
public modal: ModalMessageComponent
52+
private notificationService: NotificationService
5653
) {}
5754
/* eslint-enable */
5855

@@ -75,17 +72,13 @@ export class MatrixComponent implements OnInit {
7572
console.log(`${perfNow()}: Page loaded: Matrix`);
7673
})
7774
.catch(err => {
78-
this.displayMessage(new DialogInfo(err.message, 'An error occurred'));
75+
this.notificationService.notify('An error occurred', err.message);
7976
if (err.hasOwnProperty('stack')) {
8077
console.warn(err);
8178
}
8279
});
8380
}
8481

85-
displayMessage(dialogInfo: DialogInfo) {
86-
this.modal.openDialog(dialogInfo);
87-
}
88-
8982
setYamlData(dataStore: DataStore) {
9083
this.dataStore = dataStore;
9184
if (!dataStore.activityStore) {

src/app/pages/settings/settings.component.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ input.progress-score {
131131
flex: 1;
132132
}
133133

134+
.version-info-section .error {
135+
font-style: italic;
136+
}
137+
134138
.edit-hint {
135139
color: #666;
136140
font-size: 0.9em;

src/app/pages/settings/settings.component.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export class SettingsComponent implements OnInit {
105105
} catch (err: any) {
106106
console.warn('Error checking latest DSOMM release', err);
107107
this.remoteReleaseCheck.latestCheckError = err?.message || 'Failed to check latest release';
108+
return;
108109
} finally {
109110
this.remoteReleaseCheck.isChecking = false;
110111
}
@@ -127,14 +128,15 @@ export class SettingsComponent implements OnInit {
127128
if (remoteTag && localTag && remoteDate && localDate) {
128129
newer = remoteTag !== localTag || remoteDate > localDate;
129130
} else {
131+
newer = true; // Show download link if we cannot compare
132+
133+
// Build error message
130134
let tmp: string[] = [];
131-
if (!remoteTag) tmp.push('remoteTag');
132-
if (!localTag) tmp.push('localTag');
133-
if (!remoteDate) tmp.push('remoteDate');
134-
if (!localDate) tmp.push('localDate');
135-
this.remoteReleaseCheck.latestCheckError = `Could not determine ${tmp.join(
136-
', '
137-
)} for comparison`;
135+
if (!remoteTag) tmp.push('DSOMM model version');
136+
if (!localTag) tmp.push('local model version');
137+
if (!remoteDate) tmp.push('DSOMM model date');
138+
if (!localDate) tmp.push('local model date');
139+
this.remoteReleaseCheck.latestCheckError = `Could not determine ${tmp.join(', ')}`; // eslint-disable-line
138140
console.warn('ERROR: ' + this.remoteReleaseCheck.latestCheckError);
139141
}
140142
this.remoteReleaseCheck.isNewerAvailable = newer;

src/app/service/loader/data-loader.service.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Injectable } from '@angular/core';
22
import { perfNow } from 'src/app/util/util';
3-
import { YamlService } from '../yaml-loader/yaml-loader.service';
3+
import { FileNotFoundError, YamlService } from '../yaml-loader/yaml-loader.service';
4+
import { GithubService } from '../settings/github.service';
45
import { MetaStore } from 'src/app/model/meta-store';
56
import { TeamProgressFile, Uuid } from 'src/app/model/types';
67
import {
@@ -11,20 +12,36 @@ import {
1112
Data,
1213
} from 'src/app/model/activity-store';
1314
import { DataStore } from 'src/app/model/data-store';
15+
import { NotificationService } from '../notification.service';
1416

1517
export class DataValidationError extends Error {
1618
constructor(message: string) {
1719
super(message);
1820
}
1921
}
2022

23+
export class MissingModelError extends Error {
24+
filename: string | null;
25+
constructor(message: string, filename: string | null = null) {
26+
super(message);
27+
this.filename = filename;
28+
}
29+
}
30+
2131
@Injectable({ providedIn: 'root' })
2232
export class LoaderService {
2333
private META_FILE: string = 'assets/YAML/meta.yaml';
34+
private DSOMM_MODEL_URL: string;
2435
private debug: boolean = false;
2536
private dataStore: DataStore | null = null;
2637

27-
constructor(private yamlService: YamlService) {}
38+
constructor(
39+
private yamlService: YamlService,
40+
private githubService: GithubService,
41+
private notificationService: NotificationService
42+
) {
43+
this.DSOMM_MODEL_URL = this.githubService.getDsommModelUrl();
44+
}
2845

2946
get datastore(): DataStore | null {
3047
return this.dataStore;
@@ -70,8 +87,18 @@ export class LoaderService {
7087
console.log(`${perfNow()}: YAML: All YAML files loaded`);
7188

7289
return this.dataStore;
73-
} catch (err) {
74-
throw err;
90+
} catch (err: any) {
91+
if (err instanceof FileNotFoundError) {
92+
console.error(`${perfNow()}: Missing model file: ${err?.filename || err}`);
93+
if (err.filename && err.filename.endsWith('default/model.yaml')) {
94+
this.notificationService.notify('Loading error', `No DSOMM model found.\n\nPlease download \`model.yaml\` from [GitHub](${this.DSOMM_MODEL_URL}).`); // eslint-disable-line
95+
} else {
96+
this.notificationService.notify('Loading error', err.message + ': ' + err.filename);
97+
}
98+
} else {
99+
this.notificationService.notify('Error', 'Failed to load data: \n\n' + err);
100+
}
101+
return this.dataStore;
75102
}
76103
}
77104

@@ -117,11 +144,14 @@ export class LoaderService {
117144
private async loadActivities(meta: MetaStore): Promise<ActivityStore> {
118145
const activityStore = new ActivityStore();
119146
const errors: string[] = [];
120-
let usingHistoricYamlFile = false;
147+
let usingLegacyYamlFile = false;
121148

149+
if (meta.activityFiles.length == 0) {
150+
throw new MissingModelError('No `activityFiles` are specified in `meta.yaml`.');
151+
}
122152
for (let filename of meta.activityFiles) {
123153
if (this.debug) console.log(`${perfNow()}s: Loading activity file: ${filename}`);
124-
usingHistoricYamlFile ||= filename.endsWith('generated/generated.yaml');
154+
usingLegacyYamlFile ||= filename.endsWith('generated/generated.yaml');
125155

126156
const response: ActivityFile = await this.loadActivityFile(filename);
127157

@@ -140,8 +170,8 @@ export class LoaderService {
140170
if (errors.length > 0) {
141171
errors.forEach(error => console.error(error));
142172

143-
// Only throw for non-generated files (backwards compatibility)
144-
if (!usingHistoricYamlFile) {
173+
// Legacy generated.yaml has several data validation problems. Do not report these
174+
if (!usingLegacyYamlFile) {
145175
throw new DataValidationError(
146176
'Data validation error after loading: ' +
147177
filename +
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Injectable } from '@angular/core';
2+
import { Subject } from 'rxjs';
3+
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
4+
import {
5+
ModalMessageComponent,
6+
DialogInfo,
7+
} from '../component/modal-message/modal-message.component';
8+
9+
@Injectable({ providedIn: 'root' })
10+
export class NotificationService {
11+
private messageSubject = new Subject<{ title: string; message: string; error: any }>();
12+
message$ = this.messageSubject.asObservable();
13+
14+
constructor(private dialog: MatDialog) {}
15+
16+
notify(title: string, message: string, error: any = null) {
17+
this.messageSubject.next({ title, message, error });
18+
19+
const dialogConfig = new MatDialogConfig();
20+
dialogConfig.id = 'modal-message';
21+
dialogConfig.disableClose = true;
22+
dialogConfig.autoFocus = false;
23+
dialogConfig.data = new DialogInfo(message, title);
24+
25+
this.dialog.open(ModalMessageComponent, dialogConfig);
26+
}
27+
}

src/app/service/settings/github.service.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,19 @@ export interface GithubReleaseInfo {
1414
providedIn: 'root',
1515
})
1616
export class GithubService {
17-
private readonly CHANGELOG_URL =
18-
'https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel-data/blob/main/CHANGELOG.md';
19-
20-
private readonly LATEST_RELEASE_URL =
21-
'https://api.github.com/repos/devsecopsmaturitymodel/DevSecOps-MaturityModel-data/releases/latest';
22-
23-
private readonly DOWNLOAD_URL_TEMPLATE =
24-
'https://raw.githubusercontent.com/devsecopsmaturitymodel/DevSecOps-MaturityModel-data/{tag}/generated/activities.yaml';
17+
/* eslint-disable */
18+
private readonly DSOMM_MODEL_URL = 'https://github.com/devsecopsmaturitymodel/DevSecOps-MaturityModel-data/';
19+
private readonly CHANGELOG_URL = this.DSOMM_MODEL_URL + '/blob/main/CHANGELOG.md';
20+
private readonly LATEST_RELEASE_URL = this.DSOMM_MODEL_URL.replace('//github.com', '//api.github.com/repos') + 'releases/latest';
21+
private readonly DOWNLOAD_URL_TEMPLATE = this.DSOMM_MODEL_URL.replace('//github.com', '//raw.githubusercontent.com') + '{tag}/generated/model.yaml';
22+
/* eslint-enable */
2523

2624
constructor(private http: HttpClient) {}
2725

26+
public getDsommModelUrl(): string {
27+
return this.DSOMM_MODEL_URL;
28+
}
29+
2830
async getLatestRelease(): Promise<GithubReleaseInfo> {
2931
const obs = this.http.get<GithubReleaseInfo>(this.LATEST_RELEASE_URL);
3032
const remote: any = await firstValueFrom(obs);

src/app/service/yaml-loader/yaml-loader.service.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import {
66
} from 'yaml';
77
import { perfNow } from 'src/app/util/util';
88

9+
export class FileNotFoundError extends Error {
10+
filename: string | null;
11+
constructor(message: string, filename: string | null = null) {
12+
super(message);
13+
this.filename = filename;
14+
}
15+
}
16+
917
@Injectable({ providedIn: 'root' })
1018
export class YamlService {
1119
private _refs: Record<string, any>;
@@ -56,8 +64,13 @@ export class YamlService {
5664
const response: Response = await fetch(url);
5765

5866
if (!response.ok) {
59-
throw new Error(`Failed to fetch the '${url}' YAML file: ${response.statusText}`);
67+
if (response.status === 404) {
68+
throw new FileNotFoundError('File not found', url);
69+
} else {
70+
throw new Error(`Failed to fetch the '${url}' YAML file: ${response.statusText}`);
71+
}
6072
}
73+
6174
const yamlText: string = await response.text();
6275
const timeFetched: Date = new Date();
6376
console.debug(`${perfNow()}: YAML: Retrieved ${url}`);

0 commit comments

Comments
 (0)