User interface for an open source distributed tracing & observability platform!
More about hypertrace »
Install Node
and npm
, if not done already (Node and Npm). Recommended node version is 16+
.
Fork or clone the repository and Use following commands.
cd <dir_path>
npm ci
once done, start a development server
npm start
Navigate to http://localhost:4200/
. The app will automatically reload if you change any of the source files.
To run unit test cases (execute the unit tests via Jest)
npm run test
Hypertrace-ui uses angular
as the framework. On top of angular, following technologies are being used.
Typescript
: As a core language, hypertrace-ui uses typescript instead of traditional javascript. Learn more about Typescript hereRxJS
: For reactive programming, hypertrace-ui uses RxJs library. Learn more about RxJs hereD3.js
: Charts are the core part for hypertrace-ui. Charts are custom build here and use D3.js library. Learn more about D3.js hereSpectator
: For unit testing, hypertrace-ui uses spectator library. Learn more about Spectator hereHyperdash & Hyperdash-Angular
: Hyperdash is dashboarding framework/library and hyperdash-angular is a wrapper, specific to angular. Learn more about dasboards in dashboards section.
Hypertrace-UI code is divided into many smaller projects which gives the code base a better structure. Here are the projects.
Assest Library
: This consists of the assets which are being used in the application. For example; images, icons etc. Check this out hereCommon
: This consists of the common code/features such as application constants, colors, telemetry, utilities (with common angular pipes) etc. Check this out hereComponents
: Hypertrace-ui has a wide variety of custom made angular components. This is the place for generic components (eg.Input
Component) and directives(eg.LoadAsync
Directive). Check this out hereDashboards
: This consists of the common code for dashboards such as base model, properties etc. Check this out hereGraphql-Client
: Hypertrace-ui uses apollo graphql for API calls. This is the place where all the base graphql request related code is present such as graphql arguments, resolvers, builders etc. Check this out hereObservability
: This consists of all the different pages, components, services related to distributed tracing and observability. This project is the home for charts as well. Check this out hereTest Utils
: This consists of some unit test utilities for dashboards etc.. Check this out hereUI App
: This is not a project but a entry point for hypertrace-ui app. This consists of the home page, routes, config module etc. Check this out here
NOTE
: Each project contains a barrel file named public-api.ts
. This handles all the exports at a single place which improves the importing in the app.
For example
@import { Color } from '@hypertrace/common'
Let's talk about the essentials for development in hypertrace-ui.
Since hypertrace-ui uses angular
as core framework, all the concepts specific to angular are being used and applied in hypertrace-ui. Such as components
, directives
, pipes
, dependency injection
, services
, modules
, lazy loading
etc. Check out the angular docs for more info.
NOTE
: Test file name ends with .test.ts
instead of .spec.ts
for better readability.
Hypertrace-UI uses dashboards to build custom pages. These dashboards are widely used in the application. These dashboards are build using Hyperdash
and Hyperdash-Angular
libraries. Check out both here (Hyperdash & Hyperdash angular)
Let's check this example.
in Template
<ht-navigable-dashboard [navLocation]="this.location" [defaultJson]="this.defaultJson"> </ht-navigable-dashboard>
in Component
public readonly location: string = 'HELLO_LOCATION';
public readonly defaultJson: ModelJson = {
type: 'hello-widget',
name: 'name'
children: [],
data: {
'upper-case': false,
type: 'hello-data-source'
}
}
Now let's break this down.
- It will create a dasboard for a unique location.
- Dasboards are designed in a way that it takes a modal json as input property and renders the corresponding widgets.
- There are 3 core concepts - widget , widget renderer and data source.
Let's talk about these individually.
To create a widget, we need to create model class.
Continue with the above ModelJson
. Let's create hello-widget.model.ts
import { Model, ModelProperty, STRING_PROPERTY } from '@hypertrace/hyperdash';
@Model({
type: 'hello-widget',
})
export class HelloWidgetModel {
@ModelProperty({
type: STRING_PROPERTY.type,
key: 'name',
required: false,
})
public name?: string;
}
Now let's break this down.
- We have used decorator
Model
by which we're registering this widget (on build) for usage. type
property is the unique string to define each widget. If we look closely we have used the same string as a type in the model json as well.- We have used decorator
ModelProperty
for defining the custom properties like in this casename
.
But the question is how this class will render the dom? let's find out in next section!
Now after creating the widget model, let's create widget renderer component.
Using the same example. Let's create `hello-widget-renderer.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Renderer } from '@hypertrace/hyperdash';
import { Observable } from 'rxjs';
import { WidgetRenderer } from '../widget-renderer';
import { HelloWidgetModel } from './hello-widget.model';
@Renderer({ modelClass: HelloWidgetModel })
@Component({
selector: 'ht-hello-widget-renderer',
changeDetection: ChangeDetectionStrategy.OnPush,
template: ` <div *htLoadAsync="this.data$ as data">{{ data }} {{ this.model.name }}</div> `,
})
export class HelloWidgetRendererComponent extends WidgetRenderer<HelloWidgetModel> {
protected fetchData(): Observable<string> {
return this.api.getData();
}
}
Now let's break this down.
- We have used decorator
Renderer
by which we're registering this widget renderer (on build) for usage. modelClass
property is the same class for which we're building this renderer, in this case it isHelloWidgetModel
.- Now after extending the
WidgetRenderer
class, we have access to thename
property which we defined in the model class. fetchData
method is used to give us access tothis.data$
observable.htLoadAsync
directive is used to resolve the data observable, which can be done withasync
pipe as well.
How are we getting data? Now let's understand this in the next section.
Now after creating the widget renderer we need the data which we are using in the renderer component.
Continue with the above ModelJson
. Let's create hello-data-source.model.ts
import { Model, ModelProperty, STRING_PROPERTY} from '@hypertrace/hyperdash';
import { Observable, of } from 'rxjs';
@Model({
type: 'hello-data-source'
})
export class HelloDataSourceModel {
@ModelProperty({
key: 'upper-case',
required: false,
type: STRING_PROPERTY.type
})
public upperCase: boolean = false;
public getData(): Observable<string> {
return of(this.upperCase ? 'HELLO' ? 'Hello')
}
}
Now let's break this down.
- We have used decorator
Model
by which we're registering this data source (on build) for usage. type
property is the unique string to define each data source. If we look closely we have used the same string as a type in the model json as well for keydata
.- We have used decorator
ModelPropery
for defining custom properties; like in this caseupper-case
. - We have implemented
getData
method, and the same method is being used in the renderer componentreturn this.api.getData()
.
Now after implemening all this we can use these as shown above and render custom data.
Why we're doing this? Answer is simple, once we do all this, we can just write few lines of json to render a whole widget. For more complex examples please check home.dashboard.ts
and its constituent widgets.
Table is a custom component in the hypertrace-ui. The following example shows how to add table inside another component.
Here is what table API Says:
@Input()
public data?: TableDataSource<TableRow>;
@Input()
public columnConfigs?: TableColumnConfig[];
So Let's use this.
in Template
<ht-table [data]="this.datasource" [columnConfigs]="this.columnConfigs"> </ht-table>
in Component
public datasource?: TableDataSource<TableRow> = {
getData : () => of({
data: [{name: 'test-name1'}, {name: 'test-name2'}],
totalCount: 2
})
};
public readonly columnConfigs: TableColumnConfig[] = [
{
id: 'name',
title: 'Name'
visible: true,
sortable: false,
width: '48px',
}
];
Now let's break this down.
- It will create a table with a single column (In column configs we have only mentioned one column) and two rows (in the data source we have mentioned data as array of two objects.).
- If we look closely, table column config's id is same as the key we have used the key in data which is
name
. This is a must for the table to render the data correctly.
There are more features in the table component, like custom controls, configurations (for pagination and many other). We highly recommend you to check out the table.component.ts
to learn about tables and check out all the different examples present in the application.
Continuing from tables, let's talk about custom table cell renderers. In hypertrace-ui, we can create a custom table cell renderer to handle presentation of a cell data. These are nothing but angular component with another decorator TableCellRenderer
Now let's see how we can create a custom cell renderer. for example hello-table-cell-renderer.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TableCellRenderer } from '../../table-cell-renderer';
import { TableCellRendererBase } from '../../table-cell-renderer-base';
import { CoreTableCellParserType } from '../../types/core-table-cell-parser-type';
import { TableCellAlignmentType } from '../../types/table-cell-alignment-type';
@Component({
selector: 'ht-hello-table-cell-renderer',
styleUrls: ['./hello-table-cell-renderer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: ` <div>Hello {{ this.value }}</div> `,
})
@TableCellRenderer({
type: 'hello',
alignment: TableCellAlignmentType.Left,
parser: CoreTableCellParserType.NoOp,
})
export class HelloTableCellRendererComponent extends TableCellRendererBase<string> {}
Now let's break this down.
- We have used decorator
TableCellRenderer
by which we're registering this cell renderer (on build) for usage. type
property is the unique string to define each cell renderer.alignment
is used for the cell alignment, could be - left, right and centerparser
is used to parse the data. It can also be defined similar to a cell renderer. Suppose we are getting data as['test1', 'test2']
and we want it to be marshalled into{value1: 'test1', value2: 'test2'}
then we can create a custom parser and can handle the transformation. For no operation we useCoreTableCellParserType.NoOp
Usage
: Once this is done, we can use this in our table using display
property. This will be same as the type of the cell renderer.
Module import
TableModule.withCellRenderers([HelloTableCellRendererComponent]);
in Component
public readonly columnConfigs: TableColumnConfig[] = [
{
id: 'name',
title: 'Name'
display: 'hello'
visible: true,
sortable: false,
width: '48px',
}
];
NOTE
: We highly recommend you to check out all the existing example of table and cell renderers to learn more.
There are two type of graphql handlers we use.
Query
: To get the data from server / backend.Mutation
: For delete, post and put use cases.
Let's see an example of a query graphql-handler:
import { Injectable } from '@angular/core';
import { GraphQlHandlerType, GraphQlQueryHandler, GraphQlSelection } from '@hypertrace/graphql-client';
@Injectable({ providedIn: 'root' })
export class HelloGraphQlQueryHandlerService implements GraphQlQueryHandler<GraphQlHelloRequest, GraphQlHelloResponse> {
public readonly type: GraphQlHandlerType.Query = GraphQlHandlerType.Query;
public matchesRequest(request: unknown): request is GraphQlHelloRequest {
return (
typeof request === 'object' &&
request !== null &&
(request as Partial<GraphQlHelloRequest>).requestType === HELLO_GQL_REQUEST
);
}
public convertRequest(request: GraphQlHelloRequest): GraphQlSelection {
return {
path: 'getHello',
children: [
{
path: 'result',
},
],
};
}
public convertResponse(response: ExportSpansResponse): string | undefined {
return response.result;
}
}
export const HELLO_GQL_REQUEST = Symbol('GraphQL Hello Request');
export interface GraphQlHelloRequest {
requestType: typeof HELLO_GQL_REQUEST;
}
export interface GraphQlHelloResponse {
result: string;
}
Let's break this down.
- We have used a unique symbol, as a type for the request.
- Two interfaces
GraphQlHelloRequest
andGraphQlHelloResponse
for request and response. matchesRequest
method is used to verify the request.convertRequest
method is used for converting request into graphql selection.convertResponse
method is used to convert the response into desired format.
Usage
: Now once this is done, we can use this in the service/component
Module import
GraphQlModule.withHandlerProviders([HelloGraphQlQueryHandlerService]);
in Component/Service
// Injection
private readonly graphQlQueryService: GraphQlRequestService
// Usage
this.graphQlQueryService.query<HelloGraphQlQueryHandlerService>({
requestType: HELLO_GQL_REQUEST
})
Testing is an integral part of hypertrace-ui and hypertrace-ui maintains a good amount of code coverage using unit tests! We use Spectator
library to test components. services, directives, pipes, dashboards etc. We always write shallow
tests.
Naming
: Always write a file and class name which is easy to understand and specific to use case. for example - asynchronous loading -> directive name isLoadAsyncDirective
. Useht
as prefix in component selectors, pipes, directives etc. There is lint rule as well.Linting
: For a consistent in file code structure, linting is used and a requirement before merging the code.
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
These are some extra things that might be useful.
Hypertrace UI uses gradle to build a docker image. Gradle wrapper is already part of the source code. To build Hypertrace UI image, run:
./gradlew dockerBuildImages