hypertrace / hypertrace-ui

UI for Hypertrace
Other
25 stars 12 forks source link

Logo

Hypertrace-UI

User interface for an open source distributed tracing & observability platform!
More about hypertrace »

Table of Contents

Setup

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

Technologies

Hypertrace-ui uses angular as the framework. On top of angular, following technologies are being used.

  1. Typescript : As a core language, hypertrace-ui uses typescript instead of traditional javascript. Learn more about Typescript here
  2. RxJS : For reactive programming, hypertrace-ui uses RxJs library. Learn more about RxJs here
  3. D3.js : Charts are the core part for hypertrace-ui. Charts are custom build here and use D3.js library. Learn more about D3.js here
  4. Spectator: For unit testing, hypertrace-ui uses spectator library. Learn more about Spectator here
  5. Hyperdash & Hyperdash-Angular: Hyperdash is dashboarding framework/library and hyperdash-angular is a wrapper, specific to angular. Learn more about dasboards in dashboards section.

Code Architecture

Hypertrace-UI code is divided into many smaller projects which gives the code base a better structure. Here are the projects.

  1. Assest Library : This consists of the assets which are being used in the application. For example; images, icons etc. Check this out here
  2. Common : This consists of the common code/features such as application constants, colors, telemetry, utilities (with common angular pipes) etc. Check this out here
  3. Components : 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 here
  4. Dashboards : This consists of the common code for dashboards such as base model, properties etc. Check this out here
  5. Graphql-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 here
  6. Observability : 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 here
  7. Test Utils : This consists of some unit test utilities for dashboards etc.. Check this out here
  8. UI 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'

The Essentials

Let's talk about the essentials for development in hypertrace-ui.

Angular Specifics

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.

Dashboards

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.

Let's talk about these individually.

Widget

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.

But the question is how this class will render the dom? let's find out in next section!

Widget Renderer

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.

How are we getting data? Now let's understand this in the next section.

Data Source

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.

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.

Tables

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.

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.

Cell Renderers

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.

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.

GraphQl Handlers

There are two type of graphql handlers we use.

  1. Query : To get the data from server / backend.
  2. 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.

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

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.

Best Pratices

  1. Naming: Always write a file and class name which is easy to understand and specific to use case. for example - asynchronous loading -> directive name is LoadAsyncDirective. Use ht as prefix in component selectors, pipes, directives etc. There is lint rule as well.
  2. Linting: For a consistent in file code structure, linting is used and a requirement before merging the code.

Contributions

Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.

Others

These are some extra things that might be useful.

Building Image locally

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

Docker Image Source: