I’m fairly new to angular and javascript and building my first angular dashboard app that queries the Azure Devops api to get test results:
- Query the API to get a filtered list of release definitions (about 100)
- For each release definition get the latest release
- For each release get a collection of all the test runs from this release.
- Show a table of all results (each release definition is a row with the test results as expandable table) as soon as the first set of test results are received.
I managed to get this working with nested subscriptions on observables (see below), but I understand this should be avoided and is better done with something likt mergeMap
/ switchMap
and/or forkJoin
. Been struggling with that for days, but no luck yet.
And then there’s challenge #2: a second stream of data should be added to this: pipelines. Following the same recipe: get a list of pipelines, for each the latest pipeline run, for each of that all the test runs. Both data streams can/should be obtained separately and asynchronously and as soon as one of them has fetched the first set of test runs it can be shown on the dashboard.
How to accomplish this??
My working solution for release definitions only using nested subscriptions:
ngOnInit(): void {
this.router.paramMap.pipe(takeUntil(this.ngUnsubscribe))
.subscribe(params => {
this.teamToFilterOn = params.get('team');
this.apiService.getReleaseDefinitions(this.teamToFilterOn as string)
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((releaseDefinitions: any)=> {
if (releaseDefinitions.length === 0) {
this.isLoading = false
}
releaseDefinitions.forEach((releaseDefinition: any) => {
if (releaseDefinition.lastRelease) {
this.apiService.getRelease(releaseDefinition.lastRelease.id)
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((info: PipelineOrReleaseInfo) => {
if (info) {
this.apiService.getTestRunsByRelease(info.releaseId)
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((testruns: any) => {
this.isLoading = false;
this.results = [...this.results, { info: info, testruns: testruns, totals: this.calculateEnvironmentTotals(testruns.testRunResults)}];
this.dataSource.data = this.results;
});
}
});
}
});
});
});
}
First try using forkJoin
but stuck on how to proceed. Also not sure if this is correct, because forkJoin
seems to wait until both observables are complete, but instead as soon as one of them has a result it should proceed to loop over the results and do the remaining calls.
ngOnInit(): void {
this.router.paramMap.pipe(takeUntil(this.ngUnsubscribe))
.subscribe(params => {
this.teamToFilterOn = params.get('team');
let releaseDefQuery = this.apiService.getReleaseDefinitions(this.teamToFilterOn as string)
let pipelineDefQuery = this.apiService.getPipelineDefinitions(this.teamToFilterOn as string)
forkJoin([releaseDefQuery, pipelineDefQuery]).subscribe(definitions => {
let releaseDefinitions = definitions[0];
let pipelineDefinitions = definitions[1];
releaseDefinitions.forEach((releaseDefinition: any) => {
if (releaseDefinition.lastRelease) {
this.apiService.getRelease(releaseDefinition.lastRelease.id)
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((info: PipelineOrReleaseInfo) => {
...
2
Answers
Like any kind of
Stream
(e.g.Promise
s), when you see nesting inObservable
s you might want to take a step back to see if it’s really warranted.Let’s examine your solution bit by bit.
Our starting point is:
Then you subscribe, but within that subscribe you do observable operations on the given data, this strongly suggests you should be
pipe
ing an operation instead, and then subscribe on the final result.In this case you want to
map
yourparams
to someObservable
. You also might benefit from the "interrupt early" behavior thatswitchMap
offers. Otherwise there’s alsomergeMap
as a potential option if you don’t want to "interrupt early" (it used to be more appropriately namedflatMap
).We’ll add a
filter
andmap
for good measure, to ensure we have theteam
param, and to pluck it out (since we don’t need the rest).Then comes the part with what you want to do with that team.
You have multiple "tasks" that rely on the same input, and you want them both at the same time, so reaching for
forkJoin
is a good call. But there’s alsocombineLatest
that does something similar, but combine the results "step by step" instead.You use the word "latest" for both your tasks, so we’ll indeed reach for
combineLatest
instead:Now let’s dissect these two operations.
From what I gather, you’re only interested in releases that have a
lastRelease
. You also don’t want to "switch" when a new one comes in, you want them all, let’s encode that:Here I’ll use
tap
assuming you operate independently on the results ofreleaseDef$
andpipelineDef$
. If you don’t, skip thetap
s and pass a callback to the finalsubscribe
instead.To wrap this up, let’s put it all together:
You’ll notice I only used on
takeUntil(this.ngUnsubscribe)
as the "main" observable chain will stop with that, which means operation will stop as well.If you’re unsure or encounter issues, you can still sprinkle them as the very first argument of each
.pipe
.How to conditionally merge multiple observable list using switchMap