Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export class EntityRendererComponent implements OnChanges {
@Input()
public entity?: Entity;

@Input()
public inactive: boolean = false;

@Input()
public navigable: boolean = true;

Expand Down Expand Up @@ -59,8 +62,9 @@ export class EntityRendererComponent implements OnChanges {
this.setIconType();
}
}

public onClickNavigate(): void {
this.navigable && this.entity && this.entityNavService.navigateToEntity(this.entity);
this.navigable && this.entity && this.entityNavService.navigateToEntity(this.entity, this.inactive);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does inactive do? why do we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If an entity is inactive, it means we haven't seen any spans in the time range. Therefore, some of the screens won't show any data available and we may want to navigate to alternative pages instead for those APIs when drilled in to.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is it same as under discovery entity?
The name is not clear honestly. navigationFunction(entityId, sourceRoute, isInactive) i guess this alternate url would be defined in the entity map?

}

private setName(): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import { TableCellParser, TableCellParserBase, TableRow } from '@hypertrace/components';
import { Entity, entityIdKey } from '../../../../graphql/model/schema/entity';
import { ObservabilityTableCellType } from '../../observability-table-cell-type';
import { parseEntityFromTableRow } from './entity-table-cell-renderer-util';
import { isInactiveEntity, parseEntityFromTableRow } from './entity-table-cell-renderer-util';

@TableCellParser({
type: ObservabilityTableCellType.Entity
})
export class EntityTableCellParser extends TableCellParserBase<CellData, CellData, string | undefined> {
public parseValue(cellData: CellData, row: TableRow): CellData {
return parseEntityFromTableRow(cellData, row);
const entity = parseEntityFromTableRow(cellData, row);

if (entity === undefined) {
return undefined;
}

return {
...entity,
isInactive: isInactiveEntity(row) === true
};
}

public parseFilterValue(cellData: CellData): string | undefined {
return cellData !== undefined ? cellData[entityIdKey] : undefined;
}
}

type CellData = Entity | undefined;
type CellData = MaybeInactiveEntity | undefined;

export interface MaybeInactiveEntity extends Entity {
isInactive: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Dictionary } from '@hypertrace/common';
import { TableRow } from '@hypertrace/components';
import { MetricAggregation } from '@hypertrace/distributed-tracing';
import { isNumber } from 'lodash-es';
import { Entity, Interaction } from '../../../../graphql/model/schema/entity';
import { EntitySpecificationBuilder } from '../../../../graphql/request/builders/specification/entity/entity-specification-builder';

Expand Down Expand Up @@ -27,3 +30,22 @@ export const parseEntityFromTableRow = (cell: Entity | undefined, row: Dictionar
};

const isInteraction = (neighbor: unknown): neighbor is Interaction => typeof neighbor === 'object';

export const isInactiveEntity = (row: TableRow): boolean | undefined => {
const maxEndTimeAggregation = row['max(endTime)']; // Ew.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems... ripe for improvement. Unless we change the api to pass this info in (which wouldn't be a bad idea but likely won't happen immediately), a better heuristic for an inactive entity that avoids looking at any particular field which may or may not be requested/exist on all entities would be to check:

  1. The entity includes at least one metric AND
  2. All metrics fetched are undefined

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is really really ugly. I felt better putting it in this file which already holds some ugly Entity handling code. At least it is all isolated.

Talked with the backend folks about which heuristic to use here. Checking that max(endTime) is a valid number is the most solid way to know for sure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked with the backend folks about which heuristic to use here. Checking that max(endTime) is a valid number is the most solid way to know for sure.

See above - max(endTime) is a specific subset of the general heuristic I described which allows us to remove any coupling to a specific usage. No metric will be available for an inactive entity, so it just limits us to pick one to check.


if (!includesMaxEndTimeAggregation(maxEndTimeAggregation)) {
// If the aggregation wasn't fetched, we have no way of knowing if this Entity is inactive.
return undefined;
}

return !hasValidMaxEndTimeTimestamp(maxEndTimeAggregation.value);
};

const includesMaxEndTimeAggregation = (maxEndTimeAggregation?: unknown): maxEndTimeAggregation is MetricAggregation => {
return maxEndTimeAggregation !== undefined;
};

const hasValidMaxEndTimeTimestamp = (maxEndTime?: unknown): maxEndTime is number => {
return maxEndTime !== undefined && isNumber(maxEndTime);
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TableCellAlignmentType, TableCellRenderer, TableCellRendererBase } from '@hypertrace/components';
import { Entity } from '../../../../graphql/model/schema/entity';
import { ObservabilityTableCellType } from '../../observability-table-cell-type';
import { MaybeInactiveEntity } from './entity-table-cell-parser';

@Component({
selector: 'ht-entity-table-cell-renderer',
styleUrls: ['./entity-table-cell-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="fill-container entity-cell" [ngClass]="{ 'first-column': this.isFirstColumn }">
<ht-entity-renderer [entity]="this.value" [navigable]="true"></ht-entity-renderer>
<ht-entity-renderer
[entity]="this.value"
[inactive]="this.value?.isInactive === true"
[navigable]="true"
></ht-entity-renderer>
</div>
`
})
Expand All @@ -18,4 +22,4 @@ import { ObservabilityTableCellType } from '../../observability-table-cell-type'
alignment: TableCellAlignmentType.Left,
parser: ObservabilityTableCellType.Entity
})
export class EntityTableCellRendererComponent extends TableCellRendererBase<Entity | undefined> {}
export class EntityTableCellRendererComponent extends TableCellRendererBase<MaybeInactiveEntity | undefined> {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InjectionToken } from '@angular/core';
export interface EntityMetadata {
entityType: string;
icon: string;
detailPath(id: string, sourceRoute?: string): string[];
detailPath(id: string, sourceRoute?: string, inactive?: boolean): string[];
listPath?: string[];
apisListPath?(id: string): string[];
sourceRoutes?: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ export class EntityNavigationService {
@Inject(ENTITY_METADATA) private readonly entityMetadata: EntityMetadataMap
) {
Array.from(this.entityMetadata.values()).forEach(item => {
this.registerEntityNavigationAction(item.entityType, (id, sourceRoute) =>
this.navigationService.navigateWithinApp(item.detailPath(id, sourceRoute))
this.registerEntityNavigationAction(item.entityType, (id, sourceRoute, inactive) =>
this.navigationService.navigateWithinApp(item.detailPath(id, sourceRoute, inactive))
);
});
}

private readonly entityNavigationMap: Map<
EntityType,
(id: string, sourceRoute?: string) => Observable<boolean>
(id: string, sourceRoute?: string, inactive?: boolean) => Observable<boolean>
> = new Map();

public navigateToEntity(entity: Entity): Observable<boolean> {
public navigateToEntity(entity: Entity, isInactive?: boolean): Observable<boolean> {
const entityType = entity[entityTypeKey];
const entityId = entity[entityIdKey];
const navigationFunction = this.entityNavigationMap.get(entityType);
Expand All @@ -35,13 +35,13 @@ export class EntityNavigationService {
);

return navigationFunction
? navigationFunction(entityId, sourceRoute)
? navigationFunction(entityId, sourceRoute, isInactive)
: throwError(`Requested entity type not registered for navigation: ${entityType}`);
}

public registerEntityNavigationAction(
entityType: EntityType,
action: (id: string, sourceRoute?: string) => Observable<boolean>
action: (id: string, sourceRoute?: string, inactive?: boolean) => Observable<boolean>
): void {
this.entityNavigationMap.set(entityType, action);
}
Expand Down