Skip to content

Commit

Permalink
feat: add signal store to hotspot and team alignment analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
manfredsteyer committed Sep 28, 2024
1 parent 854bde8 commit fdc6882
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 197 deletions.
2 changes: 1 addition & 1 deletion .detective/hash
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f9232bdefbe89cefc2725a2280d1e7bc976277d1, v1.1.2
01d56f87bc03708a2aa60ad75afeeaa3ac7c27ea, v1.1.2
5 changes: 3 additions & 2 deletions .detective/log
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"Manfred Steyer <[email protected]>,Sat Sep 28 10:48:17 2024 +0200 ce7f9291f8db589484a10d04077ebab2c32253e7,chore: configure vscode to use single quotes"
"Manfred Steyer <[email protected]>,Sat Sep 28 10:48:17 2024 +0200 854bde8ff89203de89c8a8f667162cfb93f6a2c4,chore: configure vscode to use single quotes"
1 1 .detective/hash
11 0 .detective/log
4 0 .vscode/settings.json
1 1 apps/frontend/src/app/features/coupling/coupling.component.ts

"Manfred Steyer <[email protected]>,Sat Sep 28 10:44:18 2024 +0200 28b7c31784b0353ad33a72f2e5cd6c8e7bcf6947,feat: add store to coupling feature"
1 1 .detective/hash
Expand Down
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
nx run-many --target=lint --all --fix || exit 1
npm run lint
nx format:write --all
git add .
10 changes: 7 additions & 3 deletions apps/frontend/src/app/features/hotspot/hotspot.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
<input
matInput
type="number"
[(ngModel)]="minScoreControl"
[ngModel]="minScore()"
(ngModelChange)="updateFilter({ minScore: $event })"
min="0"
step="1"
/>
</mat-form-field>

<mat-form-field appearance="fill" class="form-field metric">
<mat-label>Complexity Metric</mat-label>
<mat-select [(ngModel)]="metric">
<mat-select
[ngModel]="metric"
(ngModelChange)="updateFilter({ metric: $event })"
>
@for (option of metricOptions; track option.id) {
<mat-option value="{{ option.id }}">{{ option.label }}</mat-option>
}
Expand Down Expand Up @@ -63,7 +67,7 @@
mat-row
*matRowDef="let row; columns: columnsToDisplay; let i = index"
(click)="selectRow(row, i)"
[class.selected]="selectedRow === row"
[class.selected]="isSelected(i)"
></tr>
</table>
}
Expand Down
206 changes: 64 additions & 142 deletions apps/frontend/src/app/features/hotspot/hotspot.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import {
computed,
effect,
inject,
Signal,
signal,
untracked,
viewChild,
} from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
Expand All @@ -18,52 +17,28 @@ import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import {
catchError,
combineLatest,
filter,
Observable,
of,
startWith,
switchMap,
tap,
} from 'rxjs';
import { combineLatest, startWith } from 'rxjs';

import { HotspotService } from '../../data/hotspot.service';
import { LimitsStore } from '../../data/limits.store';
import { StatusStore } from '../../data/status.store';
import {
AggregatedHotspot,
AggregatedHotspotsResult,
ComplexityMetric,
FlatHotspot,
HotspotCriteria,
HotspotResult,
initAggregatedHotspotsResult,
initHotspotResult,
} from '../../model/hotspot-result';
import { initLimits, Limits } from '../../model/limits';
import { Limits } from '../../model/limits';
import { LimitsComponent } from '../../ui/limits/limits.component';
import { debounceTimeSkipFirst } from '../../utils/debounce';
import { injectShowError } from '../../utils/error-handler';
import { EventService } from '../../utils/event.service';
import { lastSegments } from '../../utils/segments';

import { HotspotFilter, HotspotStore } from './hotspot.store';

interface Option {
id: ComplexityMetric;
label: string;
}

type LoadAggregateOptions = {
minScore: number;
limits: Limits;
metric: ComplexityMetric;
};

type LoadHotspotOptions = LoadAggregateOptions & {
selectedModule: string;
};

@Component({
selector: 'app-hotspot',
standalone: true,
Expand All @@ -84,64 +59,52 @@ type LoadHotspotOptions = LoadAggregateOptions & {
styleUrl: './hotspot.component.css',
})
export class HotspotComponent {
private statusStore = inject(StatusStore);
private limitsStore = inject(LimitsStore);
private hotspotStore = inject(HotspotStore);

private hotspotService = inject(HotspotService);
private eventService = inject(EventService);

private statusStore = inject(StatusStore);
private showError = injectShowError();
paginator = viewChild(MatPaginator);

dataSource = new MatTableDataSource<AggregatedHotspot>();
detailDataSource = new MatTableDataSource<FlatHotspot>();

aggregatedResult: Signal<AggregatedHotspotsResult>;
hotspotResult: Signal<HotspotResult>;

formattedAggregated = computed(() =>
this.formatAggregated(this.aggregatedResult().aggregated)
);
formattedHotspots = computed(() =>
this.formatHotspots(this.hotspotResult().hotspots)
);

// TODO: Decide on this vs the "rendering" effects below
// hotspotsDataSource = computed(() => {
// const dataSource = new MatTableDataSource(this.formattedHotspots());
// const paginator = this.paginator();
// if (paginator) {
// dataSource.paginator = paginator;
// }
// return dataSource;
// });

selectedRow: AggregatedHotspot | null = null;

columnsToDisplay = ['module', 'count'];
detailColumns = ['fileName', 'commits', 'complexity', 'score'];

totalCommits = this.statusStore.commits;
minScoreControl = signal(10);
limits = this.limitsStore.limits;
metric = signal<ComplexityMetric>('Length');

metricOptions: Option[] = [
{ id: 'Length', label: 'File Length' },
{ id: 'McCabe', label: 'Cyclomatic Complexity' },
];

selectedModule = signal('');
loadingAggregated = signal(false);
loadingHotspots = signal(false);
totalCommits = this.statusStore.commits;
limits = this.limitsStore.limits;

paginator = viewChild(MatPaginator);
minScore = this.hotspotStore.filter.minScore;
metric = this.hotspotStore.filter.metric;
selectedModule = this.hotspotStore.filter.selectedModule;

loadingAggregated = this.hotspotStore.loadingAggregated;
loadingHotspots = this.hotspotStore.loadingHotspots;

aggregatedResult = this.hotspotStore.aggregatedResult;
hotspotResult = this.hotspotStore.hotspotResult;

formattedAggregated = computed(() =>
formatAggregated(this.aggregatedResult().aggregated)
);

formattedHotspots = computed(() =>
formatHotspots(
this.hotspotResult().hotspots,
untracked(() => this.selectedModule())
)
);

constructor() {
const loadAggregatedEvents = {
filterChanged: this.eventService.filterChanged.pipe(startWith(null)),
minScore: toObservable(this.minScoreControl).pipe(
debounceTimeSkipFirst(300)
),
minScore: toObservable(this.minScore).pipe(debounceTimeSkipFirst(300)),
limits: toObservable(this.limits).pipe(debounceTimeSkipFirst(300)),
metric: toObservable(this.metric),
};
Expand All @@ -151,27 +114,16 @@ export class HotspotComponent {
selectedModule: toObservable(this.selectedModule),
};

const aggregated$ = combineLatest(loadAggregatedEvents).pipe(
switchMap((combi) => this.loadAggregated(combi))
const loadAggregatedOptions$ = combineLatest(loadAggregatedEvents).pipe(
takeUntilDestroyed()
);

const hotspots$ = combineLatest(loadHotspotEvent).pipe(
filter((combi) => !!combi.selectedModule),
switchMap((combi) => this.loadHotspots(combi))
const loadHotspotOptions$ = combineLatest(loadHotspotEvent).pipe(
takeUntilDestroyed()
);

this.aggregatedResult = toSignal(aggregated$, {
initialValue: initAggregatedHotspotsResult,
});

this.hotspotResult = toSignal(hotspots$, {
initialValue: initHotspotResult,
});

// effect(() => {
// const aggregated = this.formattedAggregated();
// this.dataSource.data = aggregated;
// });
this.hotspotStore.rxLoadAggregated(loadAggregatedOptions$);
this.hotspotStore.rxLoadHotspots(loadHotspotOptions$);

effect(() => {
const hotspots = this.formattedHotspots();
Expand All @@ -186,72 +138,42 @@ export class HotspotComponent {
});
}

updateLimits(limits: Limits) {
updateLimits(limits: Limits): void {
this.limitsStore.updateLimits(limits);
}

selectRow(row: AggregatedHotspot, index: number) {
const selectModule = this.aggregatedResult().aggregated[index].module;
this.selectedRow = row;
this.selectedModule.set(selectModule);
}

formatAggregated(hotspot: AggregatedHotspot[]): AggregatedHotspot[] {
return hotspot.map((hs) => ({
...hs,
module: lastSegments(hs.module, 3),
}));
updateFilter(filter: Partial<HotspotFilter>): void {
this.hotspotStore.updateFilter(filter);
}

formatHotspots(hotspot: FlatHotspot[]): FlatHotspot[] {
return hotspot.map((hs) => ({
...hs,
fileName: trimSegments(hs.fileName, this.selectedRow?.module || ''),
}));
selectRow(row: AggregatedHotspot, index: number) {
const selectedModule = this.aggregatedResult().aggregated[index].module;
this.hotspotStore.updateFilter({
selectedModule,
});
}

private loadAggregated(
options: LoadAggregateOptions
): Observable<AggregatedHotspotsResult> {
const criteria: HotspotCriteria = {
metric: options.metric,
minScore: options.minScore,
module: '',
};

this.loadingAggregated.set(true);
return this.hotspotService.loadAggregated(criteria, options.limits).pipe(
tap(() => {
this.loadingAggregated.set(false);
}),
catchError((err) => {
this.loadingAggregated.set(false);
this.showError(err);
return of(initAggregatedHotspotsResult);
})
);
isSelected(index: number) {
const module = this.aggregatedResult().aggregated[index].module;
return module === this.selectedModule();
}
}

private loadHotspots(options: LoadHotspotOptions): Observable<HotspotResult> {
const criteria: HotspotCriteria = {
metric: options.metric,
minScore: options.minScore,
module: options.selectedModule,
};

this.loadingHotspots.set(true);
function formatAggregated(hotspot: AggregatedHotspot[]): AggregatedHotspot[] {
return hotspot.map((hs) => ({
...hs,
module: lastSegments(hs.module, 3),
}));
}

return this.hotspotService.load(criteria, options.limits).pipe(
tap(() => {
this.loadingHotspots.set(false);
}),
catchError((err) => {
this.loadingHotspots.set(false);
this.showError(err);
return of(initHotspotResult);
})
);
}
function formatHotspots(
hotspot: FlatHotspot[],
selectedModule: string
): FlatHotspot[] {
return hotspot.map((hs) => ({
...hs,
fileName: trimSegments(hs.fileName, selectedModule),
}));
}

function trimSegments(fileName: string, prefix: string): string {
Expand Down
Loading

0 comments on commit fdc6882

Please sign in to comment.