├── src ├── assets │ ├── .gitkeep │ ├── images │ │ ├── kafka.png │ │ └── loading.gif │ └── css │ │ ├── animations.css │ │ └── styles.css ├── templates │ ├── error.html │ ├── consumer.html │ ├── display_topics.html │ ├── home.html │ ├── main.html │ ├── display_consumers.html │ ├── lag_graph.html │ ├── available_clusters_list.html │ └── partition_table.html ├── favicon.ico ├── styles.css ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── app │ ├── classes │ │ ├── request.ts │ │ ├── partitionInterval.ts │ │ ├── home.ts │ │ ├── clusterTopicHome.ts │ │ ├── clusterConsumerHome.ts │ │ ├── serializable.ts │ │ ├── cluster.ts │ │ ├── status.ts │ │ ├── clusterHome.ts │ │ ├── partition.ts │ │ ├── topic.ts │ │ └── consumer.ts │ ├── components │ │ ├── error.component.ts │ │ ├── display_topics.component.ts │ │ ├── partition_table.component.ts │ │ ├── app.component.ts │ │ ├── available_clusters.component.ts │ │ ├── display_consumers.component.ts │ │ ├── home.component.ts │ │ ├── consumer.component.ts │ │ ├── lag_graph.component.ts │ │ └── app.component.spec.ts │ ├── routing │ │ └── routes.ts │ ├── services │ │ ├── consumer.service.ts │ │ ├── home.service.ts │ │ ├── home.service.spec.ts │ │ └── burrow.service.ts │ └── modules │ │ └── app.module.ts ├── tslint.json ├── tsconfig.app.json ├── tsconfig.spec.json ├── browserslist ├── main.ts ├── test.ts ├── karma.conf.js ├── index.html └── polyfills.ts ├── screenshots ├── graph.PNG ├── partition.PNG └── burrowHome.PNG ├── server ├── config │ └── server_config.json ├── helpers │ └── URLUtility.js └── routes │ └── api.js ├── e2e ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.e2e.json └── protractor.conf.js ├── .editorconfig ├── .dockerignore ├── tsconfig.json ├── Dockerfile ├── .gitignore ├── server.js ├── README.md ├── package.json ├── tslint.json └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/error.html: -------------------------------------------------------------------------------- 1 |

Error

2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/BurrowUI/master/src/favicon.ico -------------------------------------------------------------------------------- /screenshots/graph.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/BurrowUI/master/screenshots/graph.PNG -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /screenshots/partition.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/BurrowUI/master/screenshots/partition.PNG -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /screenshots/burrowHome.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/BurrowUI/master/screenshots/burrowHome.PNG -------------------------------------------------------------------------------- /src/assets/images/kafka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/BurrowUI/master/src/assets/images/kafka.png -------------------------------------------------------------------------------- /src/assets/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/BurrowUI/master/src/assets/images/loading.gif -------------------------------------------------------------------------------- /server/config/server_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "burrow" : { 3 | "home" : "http://your.server.com/v3/kafka" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/templates/consumer.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /src/app/classes/request.ts: -------------------------------------------------------------------------------- 1 | export class Request { 2 | 3 | constructor( 4 | public url: string, 5 | public host: string 6 | ) {} 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/app/classes/partitionInterval.ts: -------------------------------------------------------------------------------- 1 | export class PartitionInterval { 2 | 3 | constructor( 4 | public offset: number, 5 | public timestamp: number, 6 | public lag: number 7 | ) {} 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/app/components/error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'consumer_application', 5 | templateUrl: '../../templates/error.html', 6 | }) 7 | 8 | export class ErrorComponent {} 9 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/classes/home.ts: -------------------------------------------------------------------------------- 1 | import {Request} from './request'; 2 | 3 | export class Home { 4 | 5 | constructor( 6 | public error: string, 7 | public message: string, 8 | public clusters: string[], 9 | public request: Request 10 | ) {} 11 | 12 | } 13 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/classes/clusterTopicHome.ts: -------------------------------------------------------------------------------- 1 | import {Request} from './request'; 2 | 3 | export class ClusterTopicHome { 4 | constructor( 5 | public error: string, 6 | public message: string, 7 | public topics: string[], 8 | public request: Request 9 | ) {} 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/classes/clusterConsumerHome.ts: -------------------------------------------------------------------------------- 1 | import {Request} from './request'; 2 | 3 | export class ClusterConsumerHome { 4 | constructor( 5 | public error: string, 6 | public message: string, 7 | public consumers: string[], 8 | public request: Request 9 | ) {} 10 | 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | angular-cli.json 3 | bs-config.json 4 | e2e 5 | karma-test-shim.js 6 | non-essential-files.txt 7 | protractor.config.js 8 | README.md 9 | tslint.json 10 | bs-config.e2e.json 11 | CHANGELOG.md 12 | karma.conf.js 13 | LICENSE 14 | non-essential-files.osx.txt 15 | protractor.conf.js 16 | 17 | -------------------------------------------------------------------------------- /src/app/classes/serializable.ts: -------------------------------------------------------------------------------- 1 | export class Serializable { 2 | fromJSON(json: string) { 3 | console.log('Entry: ' + json + 'from JSON'); 4 | const jsonObj = JSON.parse(json); 5 | for (const propName of Object.keys(jsonObj)) { 6 | this[propName] = jsonObj[propName]; 7 | } 8 | return this; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/classes/cluster.ts: -------------------------------------------------------------------------------- 1 | export class Cluster { 2 | 3 | constructor( 4 | public zookeepers: string[], 5 | public zookeeper_port: number, 6 | public zookeeper_path: string, 7 | public brokers: string[], 8 | public broker_port: number, 9 | public offsets_topic: string 10 | ) {} 11 | 12 | } 13 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to burrow-ui!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/modules/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/app/classes/status.ts: -------------------------------------------------------------------------------- 1 | import {Partition} from './partition'; 2 | export class Status { 3 | constructor( 4 | public cluster: string, 5 | public group: string, 6 | public status: string, 7 | public complete: number | boolean, 8 | public partitions: Partition[], 9 | public partition_count: number, 10 | public maxlag: Partition, 11 | public totallag: number 12 | ) { 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/routing/routes.ts: -------------------------------------------------------------------------------- 1 | import { ConsumerComponent } from '../components/consumer.component'; 2 | import { HomeComponent } from '../components/home.component'; 3 | import { ErrorComponent } from '../components/error.component'; 4 | 5 | export const ROUTES = [ 6 | { 7 | path: '', 8 | component: HomeComponent 9 | }, 10 | { 11 | path: 'AnalyzeConsumer', 12 | component: ConsumerComponent 13 | }, 14 | { 15 | path: '**', 16 | component: ErrorComponent 17 | } 18 | ]; 19 | -------------------------------------------------------------------------------- /src/assets/css/animations.css: -------------------------------------------------------------------------------- 1 | @keyframes CardAnimation { 2 | 0% { 3 | padding-left: 100px; 4 | } 5 | 100% { 6 | padding-left: 0px; 7 | } 8 | } 9 | 10 | @keyframes LogoAnimation { 11 | 0% { 12 | opacity: 1.0; 13 | } 14 | 100% { 15 | opacity: 0.3; 16 | } 17 | } 18 | 19 | .card-animation { 20 | animation: CardAnimation 1s; 21 | } 22 | 23 | .logo-animation { 24 | display: block; 25 | margin: auto; 26 | opacity: 0.3; 27 | animation: LogoAnimation 3s; 28 | } 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine as builder 2 | 3 | RUN mkdir -p /app 4 | WORKDIR /app 5 | COPY . /app 6 | 7 | RUN npm install 8 | RUN npm install -g @angular/cli@6.1.1 9 | 10 | RUN ng build --prod 11 | 12 | FROM node:8-alpine 13 | 14 | RUN mkdir -p /app/server /app/dist 15 | WORKDIR /app 16 | 17 | COPY --from=builder /app/server.js /app/package.json /app/ 18 | COPY --from=builder /app/server /app/server 19 | COPY --from=builder /app/dist /app/dist 20 | 21 | RUN npm install --production 22 | 23 | EXPOSE 3000 24 | 25 | CMD [ "node", "server" ] 26 | 27 | -------------------------------------------------------------------------------- /server/helpers/URLUtility.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | module.exports = URLUtility; 4 | function URLUtility(baseUrl) { 5 | 6 | this.BASE_URL = baseUrl; 7 | 8 | } 9 | 10 | URLUtility.prototype.GetBase = function (result) { 11 | 12 | request(this.BASE_URL, function(error, response, body) { 13 | result(error, body) 14 | }) 15 | 16 | }; 17 | 18 | URLUtility.prototype.Get = function (endpoint, result) { 19 | 20 | request((this.BASE_URL + endpoint), function(error, response, body) { 21 | result(error, body) 22 | }) 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /src/templates/display_topics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
TopicCluster
{{ topic?.topic }}{{ topic?.cluster }}
15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/ 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/app/classes/clusterHome.ts: -------------------------------------------------------------------------------- 1 | import {PipeTransform, Injectable, Pipe} from '@angular/core'; 2 | import {Request} from './request'; 3 | import {Cluster} from './cluster'; 4 | import {Consumer} from './consumer'; 5 | import {Topic} from './topic'; 6 | 7 | export class ClusterHome { 8 | public consumers: Consumer[]; 9 | public topics: Topic[]; 10 | public isError = false; 11 | public isWarning = false; 12 | public isOkay = true; 13 | public clusterName = ''; 14 | public numConsumers = 0; 15 | public numTopics = 0; 16 | 17 | constructor( 18 | public error: string, 19 | public message: string, 20 | public cluster: Cluster, 21 | public request: Request 22 | ) { 23 | this.consumers = []; 24 | this.topics = []; 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/app/components/display_topics.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import {ClusterHome} from '../classes/clusterHome'; 3 | import {BurrowService} from '../services/burrow.service'; 4 | import {Topic} from '../classes/topic'; 5 | import { load } from '@angular/core/src/render3/instructions'; 6 | 7 | @Component({ 8 | selector: 'display_topic_list', 9 | templateUrl: '../../templates/display_topics.html', 10 | }) 11 | 12 | export class DisplayTopicsComponent implements OnInit { 13 | @Input() cluster: ClusterHome; 14 | topics: Topic[]; 15 | 16 | constructor(private burrowService: BurrowService) { } 17 | 18 | ngOnInit() { 19 | this.burrowService.topicDictionary.subscribe(topicDict => { 20 | this.topics = topicDict[this.cluster.clusterName]; 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/templates/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 | 8 | {{ listTitle }} 9 | 10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /src/templates/main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | Kafka Analysis Tool 6 | 7 |
8 | {{ consumer }} : {{ environment }} 9 | Burrow Server: {{ burrowHome }} 10 | 11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /src/app/components/partition_table.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Consumer } from '../classes/consumer'; 3 | import { ConsumerService } from '../services/consumer.service'; 4 | 5 | @Component({ 6 | selector: 'partition-table', 7 | templateUrl: '../../templates/partition_table.html', 8 | }) 9 | 10 | export class PartitionTableComponent implements OnInit { 11 | consumer: Consumer; 12 | toggle = true; 13 | 14 | constructor(private consumerService: ConsumerService) { 15 | this.consumerService.consumer.subscribe(obj => { 16 | this.consumer = obj; 17 | }); 18 | } 19 | 20 | ngOnInit(): void { 21 | 22 | } 23 | 24 | get pipeString(): string[] { 25 | return this.toggle ? ['WARN', 'STOP', 'STALL', 'ERR', 'OK'] : ['WARN', 'STOP', 'STALL', 'ERR']; 26 | } 27 | 28 | get sortTitle(): string { 29 | return this.toggle ? 'Hide OK' : 'Show OK'; 30 | } 31 | 32 | toggleSort() { this.toggle = !this.toggle; } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute, Params } from '@angular/router'; 3 | import { BurrowService } from '../services/burrow.service'; 4 | 5 | @Component({ 6 | selector: 'consumer_application', 7 | templateUrl: '../../templates/main.html', 8 | }) 9 | 10 | export class AppComponent implements OnInit { 11 | consumer: string; 12 | environment: string; 13 | burrowHome: string; 14 | 15 | constructor(private route: ActivatedRoute, private burrowService: BurrowService) { } 16 | 17 | ngOnInit() { 18 | this.burrowService.getHome().subscribe( 19 | home => { 20 | this.burrowHome = home.request.host; 21 | }, 22 | error => { 23 | this.burrowHome = 'Error'; 24 | } 25 | ); 26 | this.getParams(); 27 | } 28 | 29 | getParams(): void { 30 | this.route.queryParams.subscribe((params: Params) => { 31 | this.consumer = params['consumer']; 32 | this.environment = params['cluster']; 33 | }); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/app/components/available_clusters.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {ClusterHome} from '../classes/clusterHome'; 3 | import {HomeService} from '../services/home.service'; 4 | import {ClusterDictionary} from '../services/burrow.service'; 5 | 6 | @Component({ 7 | selector: 'available_clusters_list', 8 | templateUrl: '../../templates/available_clusters_list.html', 9 | }) 10 | 11 | export class AvailableClustersComponent implements OnInit { 12 | clusterDict: ClusterDictionary; 13 | clusterDictKeys: string[]; 14 | 15 | constructor(private homeService: HomeService) { } 16 | 17 | ngOnInit() { 18 | this.homeService.clusters.subscribe(clusterDict => { 19 | this.clusterDict = clusterDict; 20 | this.clusterDictKeys = Object.keys(this.clusterDict).sort(); 21 | }); 22 | } 23 | 24 | public viewConsumers = (cluster) => { 25 | this.homeService.viewConsumers(cluster); 26 | } 27 | 28 | public viewTopics = (cluster) => { 29 | this.homeService.viewTopics(cluster); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/components/display_consumers.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | import {BurrowService} from '../services/burrow.service'; 4 | import {Consumer} from '../classes/consumer'; 5 | import {ClusterHome} from '../classes/clusterHome'; 6 | 7 | 8 | @Component({ 9 | selector: 'display_consumer_list', 10 | templateUrl: '../../templates/display_consumers.html', 11 | }) 12 | 13 | export class DisplayConsumersComponent implements OnInit { 14 | @Input() cluster: ClusterHome; 15 | consumers: Consumer[]; 16 | 17 | constructor(private burrowService: BurrowService, private router: Router) { } 18 | 19 | ngOnInit() { 20 | this.burrowService.consumerDictionary.subscribe(consumerDict => { 21 | this.consumers = consumerDict[this.cluster.clusterName]; 22 | }); 23 | } 24 | 25 | public analyze(cluster: string, consumer: string) { 26 | const url = '/AnalyzeConsumer'; 27 | this.router.navigate([url], {queryParams: { consumer: consumer, cluster: cluster }}); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/components/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {ClusterHome} from '../classes/clusterHome'; 3 | import {HomeService} from '../services/home.service'; 4 | 5 | @Component({ 6 | selector: 'consumer_application', 7 | templateUrl: '../../templates/home.html', 8 | }) 9 | 10 | export class HomeComponent implements OnInit { 11 | listTitle: string; 12 | selectedCluster: ClusterHome; 13 | viewConsumerList: boolean; 14 | viewTopicList: boolean; 15 | 16 | constructor(private homeService: HomeService) { } 17 | 18 | ngOnInit() { 19 | // Subscribe 20 | this.homeService.selectedCluster.subscribe(cluster => { 21 | this.selectedCluster = cluster; 22 | }); 23 | 24 | this.homeService.listTitle.subscribe(title => { 25 | this.listTitle = title; 26 | }); 27 | 28 | this.homeService.viewTopicList.subscribe(viewTopics => { 29 | this.viewTopicList = viewTopics; 30 | }); 31 | 32 | this.homeService.viewConsumerList.subscribe(viewConsumers => { 33 | this.viewConsumerList = viewConsumers; 34 | }); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/app/classes/partition.ts: -------------------------------------------------------------------------------- 1 | import {PartitionInterval} from './partitionInterval'; 2 | import {Pipe, Injectable, PipeTransform} from '@angular/core'; 3 | export class Partition { 4 | // Constructor 5 | constructor( 6 | public topic: string, 7 | public partition: number, 8 | public status: string, 9 | public start: PartitionInterval, 10 | public end: PartitionInterval 11 | ) { 12 | 13 | } 14 | 15 | get isError(): boolean { 16 | return this.status === 'ERR'; 17 | } 18 | 19 | get isWarning(): boolean { 20 | return this.status !== 'ERR' && this.status !== 'OK'; 21 | } 22 | 23 | get isOkay(): boolean { 24 | return this.status === 'OK'; 25 | } 26 | } 27 | 28 | // This is used for filtering partition results 29 | @Pipe({ 30 | name: 'partitionFilter', 31 | pure: false 32 | }) 33 | 34 | @Injectable() 35 | export class PartitionFilterPipe implements PipeTransform { 36 | transform(items: any[], args: any[]): any { 37 | // filter items array, items which match and return true will be kept, false will be filtered out 38 | return items.filter(item => args.indexOf(item.status) !== -1); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/classes/topic.ts: -------------------------------------------------------------------------------- 1 | import {Request} from './request'; 2 | import {PipeTransform, Injectable, Pipe} from '@angular/core'; 3 | 4 | export class Topic { 5 | public topic = ''; 6 | public cluster = ''; 7 | 8 | constructor( 9 | public error: string, 10 | public message: string, 11 | public offsets: number[], 12 | public request: Request 13 | ) {} 14 | 15 | } 16 | 17 | // This is used for filtering partition results 18 | @Pipe({ 19 | name: 'topicSort', 20 | pure: false 21 | }) 22 | 23 | @Injectable() 24 | export class TopicSortPipe implements PipeTransform { 25 | transform(array: Array): Array { 26 | if (array == null) { return array; } 27 | array.sort((a: any, b: any) => { 28 | // Place topics that start with special characters at the end 29 | a = a.topic.replace(/[_\W]/g, String.fromCharCode(0xFFFF)); 30 | b = b.topic.replace(/[_\W]/g, String.fromCharCode(0xFFFF)); 31 | if (a.toLowerCase() < b.toLowerCase()) { 32 | return -1; 33 | } else if (a.toLowerCase() > b.toLowerCase()) { 34 | return 1; 35 | } else { 36 | return 0; 37 | } 38 | }); 39 | return array; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Get dependencies 2 | const express = require('express'); 3 | const path = require('path'); 4 | const http = require('http'); 5 | const bodyParser = require('body-parser'); 6 | 7 | // Get our API routes 8 | const api = require('./server/routes/api'); 9 | 10 | const app = express(); 11 | 12 | // Parsers for POST data 13 | app.use(bodyParser.json()); 14 | app.use(bodyParser.urlencoded({ extended: false })); 15 | 16 | // Point static path to dist 17 | app.use(express.static(path.join(__dirname, 'dist'))); 18 | 19 | // Set our api routes 20 | app.use('/api', api); 21 | 22 | // Catch all other routes and return the index file 23 | app.get('*', (req, res) => { 24 | res.sendFile(path.join(__dirname, 'dist/index.html')); 25 | }); 26 | /** 27 | * Get port from environment and store in Express. 28 | */ 29 | const port = process.env.BURROWUI_PORT || '3000'; 30 | app.set('port', port); 31 | 32 | const host = process.env.BURROWUI_HOST || '0.0.0.0'; 33 | app.set('host', host); 34 | 35 | /** 36 | * Create HTTP server. 37 | */ 38 | const server = http.createServer(app); 39 | 40 | /** 41 | * Listen on provided port and provided host. 42 | */ 43 | server.listen(port, host, () => console.log(`API running on ${host}:${port}`) 44 | ); 45 | -------------------------------------------------------------------------------- /src/app/classes/consumer.ts: -------------------------------------------------------------------------------- 1 | import {PipeTransform, Injectable, Pipe} from '@angular/core'; 2 | import {Request} from './request'; 3 | import {Status} from './status'; 4 | 5 | export class Consumer { 6 | 7 | // Constructor 8 | constructor( 9 | public error: boolean, 10 | public message: string, 11 | public status: Status, 12 | public request: Request 13 | ) { 14 | 15 | } 16 | 17 | } 18 | 19 | // This is used for filtering partition results 20 | @Pipe({ 21 | name: 'consumerSort', 22 | pure: false 23 | }) 24 | 25 | @Injectable() 26 | export class ConsumerSortPipe implements PipeTransform { 27 | transform(array: Array): Array { 28 | if (array == null) { 29 | return array; 30 | } 31 | array.sort((a: any, b: any) => { 32 | // Place consumers that start with special characters at the end 33 | a = a.status.group.replace(/[_\W]/g, String.fromCharCode(0xFFFF)); 34 | b = b.status.group.replace(/[_\W]/g, String.fromCharCode(0xFFFF)); 35 | if (a.toLowerCase() < b.toLowerCase()) { 36 | return -1; 37 | } else if (a.toLowerCase() > b.toLowerCase()) { 38 | return 1; 39 | } else { 40 | return 0; 41 | } 42 | }); 43 | return array; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BurrowUI 2 | This is a NodeJS/Angular 6 frontend UI for Kafka cluster monitoring with [Burrow](https://github.com/linkedin/Burrow "Burrow's GitHub"). 3 | Again, this project is used as a support tool to build on top of the hard work already completed by the team at linkedin. 4 | 5 | ![homepage](https://github.com/GeneralMills/BurrowUI/blob/master/screenshots/burrowHome.PNG) 6 | 7 | --- 8 | 9 | ![graph](https://github.com/GeneralMills/BurrowUI/blob/master/screenshots/graph.PNG) 10 | 11 | --- 12 | 13 | ![partitions](https://github.com/GeneralMills/BurrowUI/blob/master/screenshots/partition.PNG) 14 | 15 | ## Use With Docker 16 | 1. Get latest docker image 17 | 18 | `docker pull generalmills/burrowui` 19 | 2. Run with Parameters 20 | 21 | `sudo docker run -p 80:3000 -e BURROW_HOME="http://{burrow_host}/v3/kafka" -d generalmills/burrowui` 22 | 23 | *BurrowUI should now be live on your server at port 80* 24 | 25 | ## Build from Source 26 | 1. CD to Project Root 27 | 2. Install Dependencies 28 | 29 | `npm install` 30 | 2. Compile Project 31 | 32 | `ng build` 33 | 3. Edit Config 34 | 35 | Edit the file /server/config/server_config.json with your Burrow Host Home 36 | 4. Start App 37 | 38 | `node server.js` 39 | 40 | *BurrowUI should now be live on your server at port 3000* 41 | 42 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | proxies: { 24 | '/api/burrow/home': 'http://localhost:3000/api/burrow/home' 25 | }, 26 | reporters: ['progress', 'kjhtml'], 27 | port: 9876, 28 | colors: true, 29 | logLevel: config.LOG_INFO, 30 | captureConsole: true, 31 | autoWatch: true, 32 | browsers: ['Chrome'], 33 | singleRun: false, 34 | customLaunchers: { 35 | ChromeDebug: { 36 | base: 'Chrome', 37 | flags: [ '--remote-debugging-port=9333' ] 38 | } 39 | } 40 | }); 41 | }; -------------------------------------------------------------------------------- /src/templates/display_consumers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 |
StatusConsumerTotal LagCluster
{{ consumer?.status?.status }}{{ consumer?.status?.group }}{{ consumer?.status?.totallag | number }}{{ consumer?.status?.cluster }} 18 | 21 |
25 | -------------------------------------------------------------------------------- /src/templates/lag_graph.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Total Lag Graph

4 |
5 |
6 |
7 | 8 |
9 |
10 | 11 | Min Lag: {{ minLag | number }} 12 | 13 | 14 | Max Lag: {{ maxLag | number }} 15 | 16 | 17 | Avg Lag: {{ avgLag | number }} 18 | 19 |
20 |
21 |
22 | 29 |
30 |
31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /src/templates/available_clusters_list.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Available Clusters 4 | 5 |
6 |
7 |
8 | 9 |
10 |
11 |

{{ clusterDict[key]?.clusterName }}

12 |
13 |
14 |
    15 |
  • 16 | 17 | Consumers: {{ clusterDict[key]?.numConsumers }} 18 | 19 |
  • 20 |
  • 21 | 22 | Topics: {{ clusterDict[key]?.numTopics }} 23 | 24 |
  • 25 |
26 |
27 | 35 |
36 | 37 |
38 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kafka Analysis Tool 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 | Kafka Analysis Tool 31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/app/services/consumer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {Consumer} from '../classes/consumer'; 3 | import {Observable, BehaviorSubject, Subject} from 'rxjs'; 4 | import {BurrowService} from './burrow.service'; 5 | import {Params, ActivatedRoute} from '@angular/router'; 6 | 7 | @Injectable() 8 | export class ConsumerService { 9 | // Variables 10 | consumerName: string; 11 | clusterName: string; 12 | 13 | // Observable Consumer 14 | private _consumer: Subject = new Subject(); 15 | get consumer(): Observable { return this._consumer.asObservable(); } 16 | 17 | // Observable Lag Window 18 | private _lagWindow: BehaviorSubject = new BehaviorSubject([]); 19 | get lagWindow(): Observable { return this._lagWindow.asObservable(); } 20 | 21 | constructor(private burrowService: BurrowService, private route: ActivatedRoute) { 22 | this.route.queryParams.subscribe((params: Params) => { 23 | this.consumerName = params['consumer']; 24 | this.clusterName = params['cluster']; 25 | this.burrowService.getConsumer(this.clusterName, this.consumerName).subscribe(cons => { 26 | this._consumer.next(cons); 27 | }); 28 | }); 29 | } 30 | 31 | refreshData() { 32 | this.burrowService.getConsumer(this.clusterName, this.consumerName).subscribe(cons => { 33 | // Add Total Lag Window 34 | const window = this._lagWindow.getValue(); 35 | window.push(cons.status.totallag); 36 | this._lagWindow.next(window); 37 | 38 | // Manage Partition Data 39 | 40 | // Update Consumer 41 | this._consumer.next(cons); 42 | }); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "burrow-ui", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^6.0.3", 15 | "@angular/common": "^6.0.3", 16 | "@angular/compiler": "^6.0.3", 17 | "@angular/core": "^6.0.3", 18 | "@angular/forms": "^6.0.3", 19 | "@angular/http": "^6.0.3", 20 | "@angular/platform-browser": "^6.0.3", 21 | "@angular/platform-browser-dynamic": "^6.0.3", 22 | "@angular/router": "^6.0.3", 23 | "@ngx-progressbar/core": "^5.0.1", 24 | "@ngx-progressbar/http": "^5.0.1", 25 | "core-js": "^2.5.4", 26 | "express": "^4.16.3", 27 | "ng2-charts": "^1.6.0", 28 | "request": ">=2.87.0", 29 | "rxjs": "^6.0.0", 30 | "zone.js": "^0.8.26" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~0.6.8", 34 | "@angular/cli": "~6.0.8", 35 | "@angular/compiler-cli": "^6.0.3", 36 | "@angular/language-service": "^6.0.3", 37 | "@types/jasmine": "~2.8.6", 38 | "@types/jasminewd2": "~2.0.3", 39 | "@types/node": "~8.9.4", 40 | "codelyzer": "~4.2.1", 41 | "jasmine-core": "~2.99.1", 42 | "jasmine-spec-reporter": "~4.2.1", 43 | "karma": "^2.0.5", 44 | "karma-chrome-launcher": "~2.2.0", 45 | "karma-coverage-istanbul-reporter": "~2.0.0", 46 | "karma-jasmine": "~1.1.1", 47 | "karma-jasmine-html-reporter": "^0.2.2", 48 | "protractor": ">=5.4.0", 49 | "ts-node": "~5.0.1", 50 | "tslint": "~5.9.1", 51 | "typescript": "~2.7.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/components/consumer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Consumer } from '../classes/consumer'; 3 | import { ConsumerService } from '../services/consumer.service'; 4 | import { ActivatedRoute, Params } from '@angular/router'; 5 | import { Observable, interval } from 'rxjs'; 6 | 7 | @Component({ 8 | selector: 'consumer_application', 9 | templateUrl: '../../templates/consumer.html', 10 | }) 11 | 12 | export class ConsumerComponent implements OnInit { 13 | consumerName: string; 14 | environmentName: string; 15 | observableConsumer: Observable; 16 | consumer: Consumer; 17 | 18 | constructor(private consumerService: ConsumerService, private route: ActivatedRoute) { 19 | 20 | } 21 | 22 | ngOnInit(): void { 23 | this.getParams(); 24 | this.observableConsumer = this.consumerService.consumer; 25 | 26 | // Subscribe to changes 27 | this.observableConsumer.subscribe(consumer => { 28 | this.consumer = consumer; 29 | }); 30 | 31 | // Refresh every 10 seconds 32 | // Start by getting 5 snapshots of data at 2 second intervals; this helps build the graph quicker. Then move to the 10 second intervals. 33 | let startWindow = 0; 34 | let refresh = interval(2 * 1000).subscribe(x => { 35 | startWindow++; 36 | this.consumerService.refreshData(); 37 | if (startWindow === 5) { 38 | refresh.unsubscribe(); 39 | refresh = interval(10 * 1000).subscribe(t => { 40 | this.consumerService.refreshData(); 41 | }); 42 | } 43 | }); 44 | } 45 | 46 | getParams(): void { 47 | this.route.queryParams.subscribe((params: Params) => { 48 | this.consumerName = params['consumer']; 49 | this.environmentName = params['environment']; 50 | }); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/templates/partition_table.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Partition State Table

4 |
5 |
6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
10 |
11 | 14 |
15 |
StatePartitionLagStart OffsetEnd OffsetTopicRecorded
{{ partition?.status }}{{ partition?.partition }}{{ partition?.end?.lag | number }}{{ partition?.start?.offset | number }}{{ partition?.end?.offset | number }}{{ partition?.topic }}{{ partition?.end?.timestamp | date:'medium' }}
38 |
39 |
40 | -------------------------------------------------------------------------------- /src/app/modules/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { ROUTES } from '../routing/routes'; 4 | import { AppComponent } from '../components/app.component'; 5 | import { ConsumerComponent } from '../components/consumer.component'; 6 | import { HomeComponent } from '../components/home.component'; 7 | import { ErrorComponent } from '../components/error.component'; 8 | import { LagGraphComponent} from '../components/lag_graph.component'; 9 | import { PartitionTableComponent} from '../components/partition_table.component'; 10 | import { RouterModule } from '@angular/router'; 11 | import { FormsModule } from '@angular/forms'; 12 | import { ConsumerService } from '../services/consumer.service'; 13 | import { ChartsModule } from 'ng2-charts'; 14 | import { HttpClientModule } from '@angular/common/http'; 15 | import { NgProgressModule } from '@ngx-progressbar/core'; 16 | import { NgProgressHttpModule } from '@ngx-progressbar/http'; 17 | import { TopicSortPipe } from '../classes/topic'; 18 | import { ConsumerSortPipe } from '../classes/consumer'; 19 | import { PartitionFilterPipe } from '../classes/partition'; 20 | import { DisplayConsumersComponent } from '../components/display_consumers.component'; 21 | import { AvailableClustersComponent } from '../components/available_clusters.component'; 22 | import { DisplayTopicsComponent } from '../components/display_topics.component'; 23 | import { BurrowService } from '../services/burrow.service'; 24 | import { HomeService } from '../services/home.service'; 25 | 26 | 27 | @NgModule({ 28 | imports: [ BrowserModule, RouterModule.forRoot(ROUTES), FormsModule, ChartsModule, HttpClientModule, 29 | NgProgressModule.forRoot({ 30 | spinner: false, 31 | color: '#cbc', 32 | thick: true 33 | }), NgProgressHttpModule ], 34 | declarations: [ AppComponent, ConsumerComponent, HomeComponent, ErrorComponent, LagGraphComponent, 35 | PartitionTableComponent, PartitionFilterPipe, TopicSortPipe, ConsumerSortPipe, 36 | DisplayConsumersComponent, AvailableClustersComponent, DisplayTopicsComponent ], 37 | bootstrap: [ AppComponent ], 38 | providers: [ ConsumerService, BurrowService, HomeService ], 39 | }) 40 | export class AppModule { } 41 | -------------------------------------------------------------------------------- /src/app/services/home.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ClusterHome} from '../classes/clusterHome'; 3 | import {Observable, Subject, BehaviorSubject} from 'rxjs'; 4 | import {ClusterDictionary, ConsumerDictionary, TopicDictionary, BurrowService} from './burrow.service'; 5 | import { ConsumerComponent } from '../components/consumer.component'; 6 | 7 | @Injectable() 8 | export class HomeService { 9 | 10 | // Observable View Topic Bool 11 | private _viewTopicList: BehaviorSubject = new BehaviorSubject(false); 12 | get viewTopicList(): Observable { return this._viewTopicList.asObservable(); } 13 | 14 | // Observable View Consumers Bool 15 | private _viewConsumerList: BehaviorSubject = new BehaviorSubject(false); 16 | get viewConsumerList(): Observable { return this._viewConsumerList.asObservable(); } 17 | 18 | // Observable List Title 19 | private _listTitle: BehaviorSubject = new BehaviorSubject('Please Select a Cluster'); 20 | get listTitle(): Observable { return this._listTitle.asObservable(); } 21 | 22 | 23 | // Observable Clusters 24 | clusters: Observable; 25 | 26 | // Observable Selected Cluster 27 | private currentCluster: ClusterHome; 28 | private _selectedCluster: BehaviorSubject = new BehaviorSubject(null); 29 | get selectedCluster(): Observable {return this._selectedCluster.asObservable(); } 30 | 31 | get loadedCluster(): ClusterHome { 32 | return this.currentCluster; 33 | } 34 | 35 | constructor(private burrowService: BurrowService) { 36 | this.clusters = this.burrowService.clusters; 37 | this.burrowService.loadHomeView(); 38 | } 39 | 40 | viewConsumers(cluster: ClusterHome) { 41 | this.burrowService.loadConsumers(cluster); 42 | this.setCurrentCluster(cluster); 43 | this._viewTopicList.next(false); 44 | this._viewConsumerList.next(true); 45 | this._listTitle.next('Available Consumers'); 46 | } 47 | 48 | viewTopics(cluster: ClusterHome) { 49 | this.burrowService.loadTopics(cluster); 50 | this.setCurrentCluster(cluster); 51 | this._viewTopicList.next(true); 52 | this._viewConsumerList.next(false); 53 | this._listTitle.next('Available Topics'); 54 | } 55 | 56 | private setCurrentCluster(cluster: ClusterHome) { 57 | this._selectedCluster.next(cluster); 58 | this.currentCluster = cluster; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/components/lag_graph.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ConsumerService } from '../services/consumer.service'; 3 | import {Observable, BehaviorSubject} from 'rxjs'; 4 | import DateTimeFormat = Intl.DateTimeFormat; 5 | 6 | @Component({ 7 | selector: 'lag-graph', 8 | templateUrl: '../../templates/lag_graph.html', 9 | }) 10 | 11 | export class LagGraphComponent implements OnInit { 12 | lagWindow: Observable; 13 | maxLag = 0; 14 | minLag = 0; 15 | avgLag = 0; 16 | 17 | public lineChartData: Array; 18 | 19 | public lineChartLabels: BehaviorSubject> = new BehaviorSubject([]); 20 | get observableLabels(): Observable> { return this.lineChartLabels.asObservable(); } 21 | 22 | public lineChartLegend = false; 23 | public lineChartType = 'line'; 24 | 25 | public lineChartOptions: any = { 26 | responsive: true 27 | }; 28 | 29 | public lineChartColors: Array = [ 30 | { // grey 31 | backgroundColor: 'rgba(233,30,99,0.2)', 32 | borderColor: 'rgba(233,30,99,1)', 33 | pointBackgroundColor: 'rgba(0,150, 136, 1)', 34 | pointBorderColor: '#fff', 35 | pointHoverBackgroundColor: '#fff', 36 | pointHoverBorderColor: 'rgba(0, 150, 136, 0.8)' 37 | } 38 | ]; 39 | 40 | constructor(private consumerService: ConsumerService) { 41 | this.lagWindow = consumerService.lagWindow; 42 | this.lineChartData = [ 43 | {data: [], label: this.consumerService.consumerName} 44 | ]; 45 | } 46 | 47 | ngOnInit(): void { 48 | this.lagWindow.subscribe(obj => { 49 | this.drawLagChart(obj); 50 | this.maxLag = Math.max(...obj); 51 | this.minLag = Math.min(...obj); 52 | let value = 0; 53 | obj.forEach(num => { 54 | value += num; 55 | }); 56 | this.avgLag = Math.floor(value / obj.length); 57 | }); 58 | } 59 | 60 | drawLagChart(newEntries: number[]): void { 61 | if (newEntries.length > 0) { 62 | const newLabels = this.lineChartLabels.getValue(); 63 | const newData = this.lineChartData.slice(0); 64 | 65 | const currentTime = new Date().toLocaleTimeString(); 66 | newLabels.push(currentTime); 67 | newData[0].data = newEntries; 68 | 69 | this.lineChartLabels.next(newLabels); 70 | this.lineChartData = newData; 71 | } 72 | } 73 | } 74 | 75 | interface ColorScheme { 76 | backgroundColor: string; 77 | borderColor: string; 78 | pointHoverBorderColor: string; 79 | } 80 | -------------------------------------------------------------------------------- /src/assets/css/styles.css: -------------------------------------------------------------------------------- 1 | .home { 2 | width: 96%; 3 | margin: 2% auto; 4 | } 5 | 6 | a.mdl-layout-title { 7 | color: white; 8 | text-decoration: none; 9 | } 10 | 11 | #metrics_chips { 12 | width: 98%; 13 | margin-right: 2%; 14 | } 15 | 16 | .metric-chip.mdl-chip { 17 | float: right; 18 | margin-left: 2%; 19 | } 20 | 21 | .cards { 22 | width: 28%; 23 | float: left; 24 | margin-right: 2%; 25 | } 26 | 27 | .cluster-card.mdl-card { 28 | width: 100%; 29 | margin-bottom: 5%; 30 | } 31 | 32 | .cluster-info.mdl-list { 33 | margin: 0; 34 | padding: 0; 35 | } 36 | 37 | .consumers { 38 | width: 70%; 39 | float: left; 40 | } 41 | 42 | .available-consumers-list { 43 | width: 100%; 44 | margin-bottom: 2%; 45 | } 46 | 47 | .display_consumers_header { 48 | width: 100%; 49 | height: 40px; 50 | } 51 | 52 | .list-title { 53 | text-align: center; 54 | vertical-align: middle; 55 | line-height: 40px; 56 | font-size: 20px; 57 | } 58 | 59 | #cluster_chips { 60 | float: right; 61 | } 62 | 63 | .loading { 64 | margin: 0 auto; 65 | } 66 | 67 | .test { 68 | font-size: 40px; 69 | } 70 | 71 | .page-content { 72 | margin: 0 auto; 73 | } 74 | 75 | .nav-header { 76 | font-size: 40px; 77 | } 78 | 79 | .centered { 80 | position: fixed; 81 | top: 50%; 82 | left: 50%; 83 | /* bring your own prefixes */ 84 | transform: translate(-50%, -50%); 85 | } 86 | 87 | .partition-state-table { 88 | width: 96%; 89 | margin: 2%; 90 | } 91 | 92 | .circle-display { 93 | margin: 2%; 94 | width: 200px; 95 | height: 200px; 96 | -webkit-border-radius: 100px; 97 | -moz-border-radius: 100px; 98 | border-radius: 100px; 99 | float: left; 100 | text-align: center; 101 | vertical-align: middle; 102 | line-height: 200px; 103 | font-size: 20px; 104 | } 105 | 106 | #rolling_window { 107 | 108 | width: 20%; 109 | height: 30%; 110 | float: left; 111 | } 112 | 113 | .circle-display.okay { 114 | background: #009688; 115 | box-shadow: 10px 10px 10px #80CBC4; 116 | } 117 | 118 | .circle-display.error { 119 | background: #E91E63; 120 | box-shadow: 10px 10px 10px #F48FB1; 121 | } 122 | 123 | .circle-display.warning { 124 | background: #FFEB3B; 125 | box-shadow: 10px 10px 10px #FFF59D; 126 | } 127 | 128 | .statusGood { background-color: #009688; } 129 | 130 | .statusWarning { background-color: #FFF59D; } 131 | 132 | .statusBad { background-color: #F48FB1; } 133 | 134 | .ng-progress-bar { 135 | height: 13px; 136 | } -------------------------------------------------------------------------------- /src/app/components/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { AppComponent } from './app.component'; 2 | import { BurrowService } from '../services/burrow.service'; 3 | import { ConsumerService } from '../services/consumer.service'; 4 | import { HomeService } from '../services/home.service'; 5 | import { HttpClientModule } from '@angular/common/http'; 6 | import { from, of, throwError } from 'rxjs'; 7 | 8 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 9 | import { RouterTestingModule } from '@angular/router/testing'; 10 | import { By } from '@angular/platform-browser'; 11 | import { DebugElement } from '@angular/core'; 12 | import { Home } from '../classes/home'; 13 | import { Request } from '../classes/request'; 14 | import { ActivatedRoute } from '@angular/router'; 15 | 16 | describe('AppComponent', function () { 17 | 18 | let burrowServiceSpy: jasmine.SpyObj; 19 | const stubHomeObj = new Home( 20 | 'false', 21 | 'cluster list returned', 22 | ['test-cluster'], 23 | new Request('/v3/kafka', 'test-burrow-host') 24 | ); 25 | 26 | beforeEach(async(() => { 27 | const spy = jasmine.createSpyObj('BurrowService', ['getHome']); 28 | 29 | TestBed.configureTestingModule({ 30 | declarations: [ AppComponent ], 31 | imports: [ RouterTestingModule, HttpClientModule ], 32 | providers: [ 33 | {provide: BurrowService, useValue: spy } 34 | ] 35 | }) 36 | .compileComponents(); 37 | 38 | burrowServiceSpy = TestBed.get(BurrowService); 39 | burrowServiceSpy.getHome.and.returnValue(of(stubHomeObj)); 40 | })); 41 | 42 | it('should create the app', async(() => { 43 | const fixture = TestBed.createComponent(AppComponent); 44 | const app = fixture.debugElement.componentInstance; 45 | expect(app).toBeTruthy(); 46 | })); 47 | 48 | it('should render title', async(() => { 49 | const fixture = TestBed.createComponent(AppComponent); 50 | fixture.detectChanges(); 51 | const compiled = fixture.debugElement.nativeElement; 52 | expect(compiled.innerText).toContain('Kafka Analysis Tool'); 53 | })); 54 | 55 | it('should set burrow home if succeeds', async(() => { 56 | const fixture = TestBed.createComponent(AppComponent); 57 | const app = fixture.debugElement.componentInstance; 58 | expect(app.burrowHome).toEqual('test-burrow-host'); 59 | })); 60 | 61 | it('should set burrow home to \'Error\' if request errors', async(() => { 62 | burrowServiceSpy.getHome.and.returnValue(throwError({status: 404})); 63 | 64 | const fixture = TestBed.createComponent(AppComponent); 65 | const app = fixture.debugElement.componentInstance; 66 | expect(app.burrowHome).toEqual('Error'); 67 | })); 68 | }); 69 | -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const URLUtility = require('../helpers/URLUtility'); 3 | const config = require('../config/server_config.json'); 4 | 5 | const router = express.Router(); 6 | 7 | let burrow_url = ""; 8 | 9 | if (process.env.BURROW_HOME) { 10 | burrow_url = process.env.BURROW_HOME; 11 | } 12 | else { 13 | burrow_url = config.burrow.home; 14 | } 15 | 16 | console.log("Burrow URL: " + burrow_url); 17 | 18 | const url = new URLUtility(burrow_url); 19 | 20 | 21 | /* GET api listing. */ 22 | // Burrow Home 23 | router.get('/burrow/home', (req, res) => { 24 | url.GetBase(function (err, body) { 25 | if (!err) { 26 | res.send(body); 27 | } else { 28 | res.send("ERROR: Something Strange Happened"); 29 | } 30 | }); 31 | }); 32 | 33 | // Cluster Home 34 | router.get('/burrow/cluster/:cluster', (req, res) => { 35 | let URL = "/" + req.params["cluster"]; 36 | 37 | url.Get(URL, function (err, body) { 38 | if (!err) { 39 | res.send(body); 40 | } else { 41 | res.send("ERROR: Something Strange Happened"); 42 | } 43 | }); 44 | }); 45 | 46 | // Cluster Consumer Home 47 | router.get('/burrow/cluster/:cluster/consumer', (req, res) => { 48 | let URL = "/" + req.params["cluster"] + "/consumer"; 49 | 50 | url.Get(URL, function (err, body) { 51 | if (!err) { 52 | res.send(body); 53 | } else { 54 | res.send("ERROR: Something Strange Happened"); 55 | } 56 | }); 57 | }); 58 | 59 | // Cluster Consumer Lag/Status 60 | router.get('/burrow/cluster/:cluster/consumer/:consumer', (req, res) => { 61 | let URL = "/" + req.params["cluster"] + "/consumer/" + req.params["consumer"] + "/lag"; 62 | 63 | url.Get(URL, function (err, body) { 64 | if (!err) { 65 | res.send(body); 66 | } else { 67 | res.send("ERROR: Something Strange Happened"); 68 | } 69 | }); 70 | }); 71 | 72 | // Cluster Topic Home 73 | router.get('/burrow/cluster/:cluster/topic', (req, res) => { 74 | let URL = "/" + req.params["cluster"] + "/topic"; 75 | 76 | url.Get(URL, function (err, body) { 77 | if (!err) { 78 | res.send(body); 79 | } else { 80 | res.send("ERROR: Something Strange Happened"); 81 | } 82 | }); 83 | }); 84 | 85 | // Burrow Topic 86 | router.get('/burrow/cluster/:cluster/topic/:topic', (req, res) => { 87 | let URL = "/" + req.params["cluster"] + "/topic/" + req.params["topic"]; 88 | 89 | url.Get(URL, function (err, body) { 90 | if (!err) { 91 | res.send(body); 92 | } else { 93 | res.send("ERROR: Something Strange Happened"); 94 | } 95 | }); 96 | }); 97 | 98 | router.get('*', (req, res) => { 99 | res.send('{ "ERROR" : "Invalid API Call" }'); 100 | }); 101 | 102 | module.exports = router; 103 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | import 'core-js/es6/symbol'; 23 | import 'core-js/es6/object'; 24 | import 'core-js/es6/function'; 25 | import 'core-js/es6/parse-int'; 26 | import 'core-js/es6/parse-float'; 27 | import 'core-js/es6/number'; 28 | import 'core-js/es6/math'; 29 | import 'core-js/es6/string'; 30 | import 'core-js/es6/date'; 31 | import 'core-js/es6/array'; 32 | import 'core-js/es6/regexp'; 33 | import 'core-js/es6/map'; 34 | import 'core-js/es6/weak-map'; 35 | import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "burrow-ui": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "src/environments/environment.ts", 35 | "with": "src/environments/environment.prod.ts" 36 | } 37 | ], 38 | "optimization": true, 39 | "outputHashing": "all", 40 | "sourceMap": false, 41 | "extractCss": true, 42 | "namedChunks": false, 43 | "aot": true, 44 | "extractLicenses": true, 45 | "vendorChunk": false, 46 | "buildOptimizer": true 47 | } 48 | } 49 | }, 50 | "serve": { 51 | "builder": "@angular-devkit/build-angular:dev-server", 52 | "options": { 53 | "browserTarget": "burrow-ui:build" 54 | }, 55 | "configurations": { 56 | "production": { 57 | "browserTarget": "burrow-ui:build:production" 58 | } 59 | } 60 | }, 61 | "extract-i18n": { 62 | "builder": "@angular-devkit/build-angular:extract-i18n", 63 | "options": { 64 | "browserTarget": "burrow-ui:build" 65 | } 66 | }, 67 | "test": { 68 | "builder": "@angular-devkit/build-angular:karma", 69 | "options": { 70 | "codeCoverage": true, 71 | "main": "src/test.ts", 72 | "polyfills": "src/polyfills.ts", 73 | "tsConfig": "src/tsconfig.spec.json", 74 | "karmaConfig": "src/karma.conf.js", 75 | "styles": [ 76 | "src/styles.css" 77 | ], 78 | "scripts": [], 79 | "assets": [ 80 | "src/favicon.ico", 81 | "src/assets" 82 | ] 83 | } 84 | }, 85 | "lint": { 86 | "builder": "@angular-devkit/build-angular:tslint", 87 | "options": { 88 | "tsConfig": [ 89 | "src/tsconfig.app.json", 90 | "src/tsconfig.spec.json" 91 | ], 92 | "exclude": [ 93 | "**/node_modules/**" 94 | ] 95 | } 96 | } 97 | } 98 | }, 99 | "burrow-ui-e2e": { 100 | "root": "e2e/", 101 | "projectType": "application", 102 | "architect": { 103 | "e2e": { 104 | "builder": "@angular-devkit/build-angular:protractor", 105 | "options": { 106 | "protractorConfig": "e2e/protractor.conf.js", 107 | "devServerTarget": "burrow-ui:serve" 108 | }, 109 | "configurations": { 110 | "production": { 111 | "devServerTarget": "burrow-ui:serve:production" 112 | } 113 | } 114 | }, 115 | "lint": { 116 | "builder": "@angular-devkit/build-angular:tslint", 117 | "options": { 118 | "tsConfig": "e2e/tsconfig.e2e.json", 119 | "exclude": [ 120 | "**/node_modules/**" 121 | ] 122 | } 123 | } 124 | } 125 | } 126 | }, 127 | "defaultProject": "burrow-ui" 128 | } -------------------------------------------------------------------------------- /src/app/services/home.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { BehaviorSubject, Subject } from 'rxjs'; 3 | 4 | import { HomeService } from './home.service'; 5 | import { BurrowService } from '../services/burrow.service'; 6 | import { ClusterHome } from '../classes/clusterHome'; 7 | import { Cluster } from '../classes/cluster'; 8 | import { Request } from '../classes/request'; 9 | 10 | 11 | 12 | describe('HomeService', () => { 13 | let homeService: HomeService; 14 | let mockClusterHome: ClusterHome; 15 | 16 | beforeEach(() => { 17 | const spy = jasmine.createSpyObj('BurrowService', ['loadHomeView', 'loadConsumerView', 'loadTopicView']); 18 | 19 | TestBed.configureTestingModule({ 20 | providers: [ 21 | HomeService, 22 | {provide: BurrowService, useValue: spy } 23 | ] 24 | }); 25 | 26 | homeService = TestBed.get(HomeService); 27 | 28 | mockClusterHome = new ClusterHome( 29 | 'false', 30 | 'cluster list returned', 31 | new Cluster(['zk1'], 2181, 'zkpath', ['kafka'], 9092, ''), 32 | new Request('/v3/kafka', 'test-burrow-host') 33 | ); 34 | mockClusterHome.clusterName = 'mock-cluster'; 35 | }); 36 | 37 | it('should be created', () => { 38 | expect(homeService).toBeTruthy(); 39 | }); 40 | 41 | it('.viewTopicList should return inital viewTopicList as false', () => { 42 | homeService.viewTopicList.subscribe(toggle => { 43 | expect(toggle).toBe(false); 44 | }); 45 | }); 46 | 47 | it('.viewConsumerList should return initial viewConsumerList as false', () => { 48 | homeService.viewConsumerList.subscribe(toggle => { 49 | expect(toggle).toBe(false); 50 | }); 51 | }); 52 | 53 | it('.listTitle should return inital listTitle', () => { 54 | homeService.listTitle.subscribe(title => { 55 | expect(title).toEqual('Please Select a Cluster'); 56 | }); 57 | }); 58 | 59 | it('.selectedCluster should return initial empty cluster as empty', () => { 60 | homeService.selectedCluster.subscribe(cluster => { 61 | expect(cluster).toBeFalsy(); 62 | }); 63 | }); 64 | 65 | it('.loadedCluster should return inital loaded cluster as empty', () => { 66 | expect(homeService.loadedCluster).toBeFalsy(); 67 | }); 68 | 69 | it('#viewTopics sets selected cluster', () => { 70 | homeService.viewTopics(mockClusterHome); 71 | homeService.selectedCluster.subscribe((cluster: ClusterHome) => { 72 | expect(cluster).toEqual(mockClusterHome); 73 | }); 74 | }); 75 | 76 | it('#viewTopics sets loaded cluster', () => { 77 | homeService.viewTopics(mockClusterHome); 78 | expect(homeService.loadedCluster).toEqual(mockClusterHome); 79 | }); 80 | 81 | it('#viewTopics toggles viewTopicList to true', () => { 82 | homeService.viewTopics(mockClusterHome); 83 | homeService.viewTopicList.subscribe((toggle: boolean) => { 84 | expect(toggle).toEqual(true); 85 | }); 86 | }); 87 | 88 | it('#viewTopics toggles viewConsumerList to false', () => { 89 | homeService.viewTopics(mockClusterHome); 90 | homeService.viewConsumerList.subscribe((toggle: boolean) => { 91 | expect(toggle).toEqual(false); 92 | }); 93 | }); 94 | 95 | it('#viewTopics sets listTitle', () => { 96 | homeService.viewTopics(mockClusterHome); 97 | homeService.listTitle.subscribe((listTitle: string) => { 98 | expect(listTitle).toEqual('Available Topics'); 99 | }); 100 | }); 101 | 102 | it('#viewConsumers sets loaded cluster', () => { 103 | homeService.viewConsumers(mockClusterHome); 104 | expect(homeService.loadedCluster).toEqual(mockClusterHome); 105 | }); 106 | 107 | it('#viewConsumers toggles viewConsumerList to true', () => { 108 | homeService.viewConsumers(mockClusterHome); 109 | homeService.viewConsumerList.subscribe((toggle: boolean) => { 110 | expect(toggle).toEqual(true); 111 | }); 112 | }); 113 | 114 | it('#viewConsumers toggles viewTopicList to false', () => { 115 | homeService.viewConsumers(mockClusterHome); 116 | homeService.viewTopicList.subscribe((toggle: boolean) => { 117 | expect(toggle).toEqual(false); 118 | }); 119 | }); 120 | 121 | it('#viewConsumers sets listTitle', () => { 122 | homeService.viewConsumers(mockClusterHome); 123 | homeService.listTitle.subscribe((listTitle: string) => { 124 | expect(listTitle).toEqual('Available Consumers'); 125 | }); 126 | }); 127 | 128 | }); 129 | -------------------------------------------------------------------------------- /src/app/services/burrow.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpErrorResponse } from '@angular/common/http'; 3 | import {Observable, BehaviorSubject, Subject, of, forkJoin, concat, throwError, interval, combineLatest, merge} from 'rxjs'; 4 | import { retry, map, catchError, take, mergeMap, concatMap } from 'rxjs/operators'; 5 | import {Home} from '../classes/home'; 6 | import {ClusterHome} from '../classes/clusterHome'; 7 | import {ClusterConsumerHome} from '../classes/clusterConsumerHome'; 8 | import {Consumer} from '../classes/consumer'; 9 | import {ClusterTopicHome} from '../classes/clusterTopicHome'; 10 | import {Topic} from '../classes/topic'; 11 | 12 | @Injectable() 13 | export class BurrowService { 14 | // Observable Home 15 | private _home: Subject = new Subject(); 16 | get home(): Observable { return this._home.asObservable(); } 17 | 18 | // Observable Dictionary of Clusters 19 | private _clusters: BehaviorSubject = new BehaviorSubject({}); 20 | get clusters(): Observable { return this._clusters.asObservable(); } 21 | 22 | // Observable Dictionary of Consumers 23 | private _consumers: BehaviorSubject = new BehaviorSubject({}); 24 | get consumerDictionary(): Observable { 25 | return this._consumers.asObservable(); 26 | } 27 | 28 | // Observable Dictionary of Topics 29 | private _topics: BehaviorSubject = new BehaviorSubject({}); 30 | get topicDictionary(): Observable { 31 | return this._topics.asObservable(); 32 | } 33 | 34 | // Home URL for Burrow 35 | private burrowUrl = '/api/burrow'; 36 | 37 | constructor(private http: HttpClient) { } 38 | 39 | // Setup Methods 40 | loadHomeView(): void { 41 | this.getHome().subscribe(homeObj => { 42 | this._home.next(homeObj); 43 | 44 | homeObj.clusters.forEach(clusterName => { 45 | this.getCluster(clusterName).subscribe(newCluster => { 46 | 47 | this.getClusterConsumerHome(clusterName).subscribe(clusterObj => { 48 | newCluster.numConsumers = clusterObj.consumers.length; 49 | }); 50 | 51 | this.getClusterTopicHome(clusterName).subscribe(clusterObj => { 52 | newCluster.numTopics = clusterObj.topics.length; 53 | }); 54 | 55 | const clusterDictionary = this._clusters.getValue(); 56 | clusterDictionary[newCluster.clusterName] = newCluster; 57 | this._clusters.next(clusterDictionary); 58 | }); 59 | }); 60 | }); 61 | } 62 | 63 | loadConsumers(cluster: ClusterHome): void { 64 | const clusterName = cluster.clusterName; 65 | 66 | this.getClusterConsumerHome(clusterName).subscribe(clusterObj => { 67 | const consumerDictionary = this._consumers.getValue(); 68 | consumerDictionary[clusterName] = []; 69 | 70 | clusterObj.consumers.forEach(consumer => { 71 | this.getConsumer(clusterName, consumer).subscribe(newConsumer => { 72 | consumerDictionary[clusterName].push(newConsumer); 73 | this._consumers.next(consumerDictionary); 74 | }); 75 | }); 76 | }); 77 | 78 | } 79 | 80 | loadTopics(cluster: ClusterHome): void { 81 | const clusterName = cluster.clusterName; 82 | 83 | this.getClusterTopicHome(clusterName).subscribe(clusterObj => { 84 | const topicDictionary = this._topics.getValue(); 85 | topicDictionary[clusterName] = []; 86 | 87 | clusterObj.topics.forEach(topic => { 88 | this.getTopic(clusterName, topic).subscribe(newTopic => { 89 | topicDictionary[clusterName].push(newTopic); 90 | this._topics.next(topicDictionary); 91 | }); 92 | }); 93 | }); 94 | } 95 | 96 | getHome(): Observable { 97 | return this.http.get(this.burrowUrl + '/home') 98 | .pipe( 99 | retry(3), 100 | catchError(this.handleError) 101 | ); 102 | } 103 | 104 | getCluster(cluster: string): Observable { 105 | return this.http.get(this.burrowUrl + '/cluster/' + cluster) 106 | .pipe( 107 | map((response: ClusterHome) => { 108 | response.clusterName = cluster; 109 | return response; 110 | }), 111 | retry(3), 112 | catchError(this.handleError) 113 | ); 114 | } 115 | 116 | getClusterConsumerHome(cluster: string): Observable { 117 | return this.http.get(this.burrowUrl + '/cluster/' + cluster + '/consumer') 118 | .pipe( 119 | retry(3), 120 | catchError(this.handleError) 121 | ); 122 | } 123 | 124 | getConsumer(cluster: string, consumer: string): Observable { 125 | return this.http.get(this.burrowUrl + '/cluster/' + cluster + '/consumer/' + consumer) 126 | .pipe( 127 | retry(3), 128 | catchError(this.handleError) 129 | ); 130 | } 131 | 132 | getClusterTopicHome(cluster: string): Observable { 133 | return this.http.get(this.burrowUrl + '/cluster/' + cluster + '/topic') 134 | .pipe( 135 | retry(3), 136 | catchError(this.handleError) 137 | ); 138 | } 139 | 140 | getTopic(cluster: string, topic: string): Observable { 141 | return this.http.get(this.burrowUrl + '/cluster/' + cluster + '/topic/' + topic) 142 | .pipe( 143 | map((response: Topic) => { 144 | response.cluster = cluster; 145 | response.topic = topic; 146 | return response; 147 | }), 148 | retry(3), 149 | catchError(this.handleError) 150 | ); 151 | } 152 | 153 | private handleError(error: HttpErrorResponse) { 154 | if (error.error instanceof ErrorEvent) { 155 | // A client-side or network error occurred. Handle it accordingly. 156 | console.error('An error occurred:', error.error.message); 157 | } else { 158 | // The backend returned an unsuccessful response code. 159 | // The response body may contain clues as to what went wrong, 160 | console.error( 161 | `Backend returned code ${error.status}, ` + 162 | `body was: ${error.error}`); 163 | } 164 | // return an observable with a user-facing error message 165 | return throwError( 166 | 'There was a server error, please try again later.'); 167 | } 168 | } 169 | 170 | export interface ConsumerDictionary { 171 | [ index: string ]: Consumer[]; 172 | } 173 | 174 | export interface TopicDictionary { 175 | [ index: string ]: Topic[]; 176 | } 177 | 178 | export interface ClusterDictionary { 179 | [ index: string ]: ClusterHome; 180 | } 181 | 182 | --------------------------------------------------------------------------------