src/app/app.component.ts
AfterViewInit
changeDetection | ChangeDetectionStrategy.OnPush |
selector | ccf-root |
styleUrls | ./app.component.scss |
templateUrl | ./app.component.html |
Properties |
|
Methods |
Outputs |
constructor(lookup: OrganLookupService, ga: GoogleAnalyticsService, configState: GlobalConfigState<GlobalConfig>)
|
||||||||||||
Defined in src/app/app.component.ts:72
|
||||||||||||
Parameters :
|
nodeClicked | |
Type : EventEmitter
|
|
Defined in src/app/app.component.ts:48
|
sexChange | |
Type : EventEmitter
|
|
Defined in src/app/app.component.ts:46
|
sideChange | |
Type : EventEmitter
|
|
Defined in src/app/app.component.ts:47
|
updateInput | |||||||||
updateInput(key: string, value)
|
|||||||||
Defined in src/app/app.component.ts:149
|
|||||||||
Parameters :
Returns :
void
|
Readonly asctbUrl$ |
Default value : this.configState.getOption('asctbUrl')
|
Defined in src/app/app.component.ts:57
|
Readonly blocks$ |
Type : Observable<TissueBlockResult[]>
|
Defined in src/app/app.component.ts:67
|
Readonly donorLabel$ |
Default value : this.configState.getOption('donorLabel')
|
Defined in src/app/app.component.ts:54
|
Readonly euiUrl$ |
Default value : this.configState.getOption('euiUrl')
|
Defined in src/app/app.component.ts:56
|
Readonly filter$ |
Default value : this.configState
.getOption('highlightProviders')
.pipe(map((providers) => ({ tmc: providers ?? [] })))
|
Defined in src/app/app.component.ts:51
|
Readonly hraPortalUrl$ |
Default value : this.configState.getOption('hraPortalUrl')
|
Defined in src/app/app.component.ts:58
|
left |
Type : ElementRef<HTMLElement>
|
Decorators :
@ViewChild('left', {read: ElementRef, static: true})
|
Defined in src/app/app.component.ts:43
|
Readonly onlineCourseUrl$ |
Default value : this.configState.getOption('onlineCourseUrl')
|
Defined in src/app/app.component.ts:59
|
Readonly organ$ |
Type : Observable<SpatialEntity | undefined>
|
Defined in src/app/app.component.ts:63
|
Readonly organInfo$ |
Type : Observable<OrganInfo | undefined>
|
Defined in src/app/app.component.ts:62
|
Readonly paperUrl$ |
Default value : this.configState.getOption('paperUrl')
|
Defined in src/app/app.component.ts:60
|
right |
Type : ElementRef<HTMLElement>
|
Decorators :
@ViewChild('right', {read: ElementRef, static: true})
|
Defined in src/app/app.component.ts:44
|
Readonly ruiUrl$ |
Default value : this.configState.getOption('ruiUrl')
|
Defined in src/app/app.component.ts:55
|
Readonly scene$ |
Type : Observable<SpatialSceneNode[]>
|
Defined in src/app/app.component.ts:64
|
Readonly sex$ |
Default value : this.configState.getOption('sex')
|
Defined in src/app/app.component.ts:49
|
Readonly side$ |
Default value : this.configState.getOption('side')
|
Defined in src/app/app.component.ts:50
|
stats |
Type : AggregateResult[]
|
Default value : []
|
Defined in src/app/app.component.ts:69
|
Readonly stats$ |
Type : Observable<AggregateResult[]>
|
Defined in src/app/app.component.ts:65
|
Readonly statsLabel$ |
Type : Observable<string>
|
Defined in src/app/app.component.ts:66
|
import { Immutable } from '@angular-ru/common/typings';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Output,
ViewChild,
} from '@angular/core';
import { SpatialSceneNode } from 'ccf-body-ui';
import { AggregateResult, SpatialEntity, TissueBlockResult } from 'ccf-database';
import { GlobalConfigState, OrganInfo } from 'ccf-shared';
import { GoogleAnalyticsService } from 'ngx-google-analytics';
import { Observable, combineLatest, of } from 'rxjs';
import { map, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';
import { OrganLookupService } from './core/services/organ-lookup/organ-lookup.service';
interface GlobalConfig {
organIri?: string;
side?: string;
sex?: 'Both' | 'Male' | 'Female';
highlightProviders?: string[];
donorLabel?: string;
ruiUrl?: string;
euiUrl?: string;
asctbUrl?: string;
hraPortalUrl?: string;
onlineCourseUrl?: string;
paperUrl?: string;
}
const EMPTY_SCENE = [{ color: [0, 0, 0, 0], opacity: 0.001 }];
@Component({
selector: 'ccf-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements AfterViewInit {
@ViewChild('left', { read: ElementRef, static: true }) left!: ElementRef<HTMLElement>;
@ViewChild('right', { read: ElementRef, static: true }) right!: ElementRef<HTMLElement>;
@Output() readonly sexChange = new EventEmitter<'Male' | 'Female'>();
@Output() readonly sideChange = new EventEmitter<'Left' | 'Right'>();
@Output() readonly nodeClicked = new EventEmitter();
readonly sex$ = this.configState.getOption('sex');
readonly side$ = this.configState.getOption('side');
readonly filter$ = this.configState
.getOption('highlightProviders')
.pipe(map((providers) => ({ tmc: providers ?? [] })));
readonly donorLabel$ = this.configState.getOption('donorLabel');
readonly ruiUrl$ = this.configState.getOption('ruiUrl');
readonly euiUrl$ = this.configState.getOption('euiUrl');
readonly asctbUrl$ = this.configState.getOption('asctbUrl');
readonly hraPortalUrl$ = this.configState.getOption('hraPortalUrl');
readonly onlineCourseUrl$ = this.configState.getOption('onlineCourseUrl');
readonly paperUrl$ = this.configState.getOption('paperUrl');
readonly organInfo$: Observable<OrganInfo | undefined>;
readonly organ$: Observable<SpatialEntity | undefined>;
readonly scene$: Observable<SpatialSceneNode[]>;
readonly stats$: Observable<AggregateResult[]>;
readonly statsLabel$: Observable<string>;
readonly blocks$: Observable<TissueBlockResult[]>;
stats: AggregateResult[] = [];
private latestConfig: Immutable<GlobalConfig> = {};
private latestOrganInfo?: OrganInfo;
constructor(
lookup: OrganLookupService,
private readonly ga: GoogleAnalyticsService,
private readonly configState: GlobalConfigState<GlobalConfig>,
) {
this.organInfo$ = configState.config$.pipe(
tap((config) => (this.latestConfig = config)),
switchMap((config) =>
lookup.getOrganInfo(config.organIri ?? '', config.side?.toLowerCase?.() as OrganInfo['side'], config.sex),
),
tap((info) => this.logOrganLookup(info)),
tap((info) => (this.latestOrganInfo = info)),
shareReplay(1),
);
this.organ$ = this.organInfo$.pipe(
switchMap((info) =>
info ? lookup.getOrgan(info, info.hasSex ? this.latestConfig.sex : undefined) : of(undefined),
),
tap((organ) => {
if (organ && this.latestOrganInfo) {
const newSex = this.latestOrganInfo?.hasSex ? organ.sex : undefined;
if (newSex !== this.latestConfig.sex) {
this.updateInput('sex', newSex);
}
if (organ.side !== this.latestConfig.side) {
this.updateInput('side', organ.side);
}
}
}),
shareReplay(1),
);
this.scene$ = this.organ$.pipe(
switchMap((organ) =>
organ && this.latestOrganInfo
? lookup.getOrganScene(this.latestOrganInfo, organ.sex)
: of(EMPTY_SCENE as SpatialSceneNode[]),
),
);
this.stats$ = combineLatest([this.organ$, this.donorLabel$]).pipe(
switchMap(([organ, donorLabel]) =>
organ && this.latestOrganInfo
? lookup
.getOrganStats(this.latestOrganInfo, organ.sex)
.pipe(
map((agg) =>
agg.map((result) =>
donorLabel && result.label === 'Donors' ? { ...result, label: donorLabel } : result,
),
),
)
: of([]),
),
);
this.statsLabel$ = this.organ$.pipe(
map((organ) => this.makeStatsLabel(this.latestOrganInfo, organ?.sex)),
startWith('Loading...'),
);
this.blocks$ = this.organ$.pipe(
switchMap((organ) =>
organ && this.latestOrganInfo ? lookup.getBlocks(this.latestOrganInfo, organ.sex) : of([]),
),
);
}
ngAfterViewInit(): void {
const { left, right } = this;
const rightHeight = right.nativeElement.offsetHeight;
left.nativeElement.style.height = `${rightHeight}px`;
}
updateInput(key: string, value: unknown): void {
this.configState.patchConfig({ [key]: value });
}
private makeStatsLabel(info: OrganInfo | undefined, sex?: string): string {
let parts: (string | undefined)[] = [`Unknown IRI: ${this.latestConfig.organIri}`];
if (info) {
// Use title cased side for a cleaner display
const side = info.side ? info.side.charAt(0).toUpperCase() + info.side.slice(1) : undefined;
parts = [sex, info.organ, side];
}
return parts.filter((seg) => !!seg).join(', ');
}
private logOrganLookup(info: OrganInfo | undefined): void {
const event = info ? 'organ_lookup_success' : 'organ_lookup_failure';
const inputs = `Iri: ${this.latestConfig.organIri} - Sex: ${this.latestConfig.sex} - Side: ${this.latestConfig.side}`;
this.ga.event(event, 'organ', inputs);
}
}
<div class="container">
<div class="left" #left>
<ccf-organ
[blocks]="(blocks$ | async) ?? []"
[filter]="$any(filter$ | async)"
[sex]="(sex$ | async)!"
[side]="$any(side$ | async)"
[organ]="(organ$ | async) ?? undefined"
[scene]="(scene$ | async) ?? []"
(sexChange)="updateInput('sex', $event); sexChange.emit($event)"
(nodeClick)="nodeClicked.emit($event)"
(sideChange)="updateInput('side', $event); sideChange.emit($event)"
>
</ccf-organ>
</div>
<div class="right" #right>
<ccf-stats-list [statsLabel]="(statsLabel$ | async) ?? ''" [stats]="(stats$ | async) ?? []"> </ccf-stats-list>
<ccf-link-cards
[ruiUrl]="(ruiUrl$ | async) ?? ''"
[euiUrl]="(euiUrl$ | async) ?? ''"
[asctbUrl]="(asctbUrl$ | async) ?? ''"
[hraPortalUrl]="(hraPortalUrl$ | async) ?? ''"
[onlineCourseUrl]="(onlineCourseUrl$ | async) ?? ''"
[paperUrl]="(paperUrl$ | async) ?? ''"
>
</ccf-link-cards>
</div>
</div>
./app.component.scss
.container {
height: fit-content;
display: flex;
flex-direction: row;
padding: 1rem;
font-family:
var(--ccf-ui-font, ''),
Inter,
Inter Variable,
sans-serif;
font-size: 0.95rem;
line-height: 1.5;
text-align: left;
position: relative;
background-color: white;
color: black;
.left {
width: auto;
flex-grow: 1;
}
.right {
margin-left: 2rem;
height: fit-content;
width: 29rem;
}
}