diff --git a/docker-compose.yml b/docker-compose.yml index b6e407a..2352e80 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,5 +8,19 @@ services: ports: - 8080:80 + proxy: + build: + context: ./proxy + dockerfile: Dockerfile + network: host + ports: + - 3000:3000 + networks: + - dashboardNetwork + depends_on: + - dashboard + extra_hosts: + - host.docker.internal:host-gateway + networks: dashboardNetwork: diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 0000000..fb57717 --- /dev/null +++ b/proxy/Dockerfile @@ -0,0 +1,20 @@ +# Use an official Node runtime as a parent image +FROM node:latest + +# Set the working directory in the container +WORKDIR /dist/src/app + +# Copy the current directory contents into the container at /usr/src/app +COPY . . + +RUN npm cache clean --force + +# Copy files from local machine to virtual directory in docker image +COPY . . +RUN npm install + +# Make port 3000 available to the world outside this container +EXPOSE 3000 + +# Run prometheus.proxy.js when the container launches +CMD ["node", "prometheus.proxy.js", "--docker"] diff --git a/proxy/package.json b/proxy/package.json new file mode 100644 index 0000000..184bc01 --- /dev/null +++ b/proxy/package.json @@ -0,0 +1,12 @@ +{ + "name": "proxy", + "version": "0.0.0", + "scripts": {}, + "private": true, + "dependencies": { + "axios": "^1.6.5", + "cors": "^2.8.5", + "express": "^4.18.2" + }, + "devDependencies": { } +} diff --git a/proxy/prometheus.proxy.js b/proxy/prometheus.proxy.js new file mode 100644 index 0000000..9fa71d8 --- /dev/null +++ b/proxy/prometheus.proxy.js @@ -0,0 +1,66 @@ +const express = require('express'); +const axios = require('axios'); +const cors = require('cors'); +const app = express(); +const port = 3000; + +// handle arguments +const showHelp = () => { +console.log('Usage: node proxy.js [--docker]'); + console.log(' --docker: proxy to host.docker.internal instead of localhost'); + process.exit(1); +} +if (process.argv.indexOf('-h') > -1 || process.argv.indexOf('--help') > -1) { + showHelp(); +} +const isDocker = process.argv.indexOf('--docker') > -1; + +// Enable CORS for all routes +app.use(cors()); +app.use(express.json()); + +// Proxy endpoint +app.all('/proxy', async (req, res) => { + // Extracting target URL and basic auth credentials from headers + let targetUrl; + if (isDocker) { + targetUrl = req.headers['x-target-url'] = req.headers['x-target-url'].replace('localhost', 'host.docker.internal'); + } + else { + targetUrl = req.headers['x-target-url']; + } + + const authHeader = req.headers['authorization']; // Basic Auth Header + + if (!targetUrl) { + return res.status(400).send({error: 'Target URL is required'}); + } + + console.log(`Proxying request to ${targetUrl}`); + + try { + // Forwarding the request to the target URL + const response = await axios({ + method: req.method, + url: targetUrl, + data: req.body, + headers: { ...req.headers, 'Authorization': authHeader }, + responseType: 'stream' + }); + + // Sending back the response from the target URL + response.data.pipe(res); + } catch (error) { + // Handling errors + if (error.response) { + // Forwarding the error response from the target server + return res.status(error.response.status).sendStatus(error.response.status); + } else { + return res.status(500).send({ error: error.message }); + } + } +}); + +app.listen(port, () => { + console.log(`Proxy server running at http://localhost:${port} (docker mode: ${isDocker})`); +}); diff --git a/src/app/core/services/auth.service.spec.ts b/src/app/core/services/auth.service.spec.ts new file mode 100644 index 0000000..f1251ca --- /dev/null +++ b/src/app/core/services/auth.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts new file mode 100644 index 0000000..e13890c --- /dev/null +++ b/src/app/core/services/auth.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private sessionKey = 'auth-credentials'; + + cacheCredentials(username: string, password: string): void { + const encodedCredentials = btoa(`${username}:${password}`); + sessionStorage.setItem(this.sessionKey, encodedCredentials); + } + + getCredentials(): string | null { + return sessionStorage.getItem(this.sessionKey); + } + + clearCredentials(): void { + sessionStorage.removeItem(this.sessionKey); + } +} diff --git a/src/app/core/services/data.service.ts b/src/app/core/services/data.service.ts index 6f6a765..7d1d43e 100644 --- a/src/app/core/services/data.service.ts +++ b/src/app/core/services/data.service.ts @@ -2,26 +2,7 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {Papa} from 'ngx-papaparse'; import {Dataset} from '../../shared/models/dataset'; -import {Observable, Subscription} from "rxjs"; - - -interface MetricType { - metric: { - __name__: string; - [key: string]: string; // Other properties of the metric - }; - values: TupleType[]; -} - -type TupleType = [number, any]; // Adjust as per your data structure - - - -interface PrometheusResponse { - status: string; - data: string[]; - error: string; -} +import {PrometheusService} from "./prometheus.service"; @Injectable({ @@ -32,6 +13,7 @@ export class DataService { constructor( private papa: Papa, private http: HttpClient, + private prometheusService: PrometheusService, ) { } parseCsvFile(file: File) { @@ -72,182 +54,56 @@ export class DataService { return blob; } - setDbUrl(dbUrl: string): Promise { - const url = dbUrl + '/api/v1/query?query=up'; - return new Promise((resolve, reject) => { - this.http.get(url).subscribe( - data => { - resolve(true); - }, - error => { - resolve(false); - } - ); - }); - } - - getAvailableMetrics(dbUrl: string): Promise { - /** - * Get available metrics from prometheus database. - * - * @param dbUrl - prometheus database url - * @returns array of metric names - */ - const url = dbUrl + '/api/v1/label/__name__/values'; - return new Promise((resolve, reject) => { - this.http.get(url).subscribe( - data => { - resolve(data['data']); - }, - error => { - resolve([]); - } - ); - }); - } - - getMetrics(dbUrl: string, metrics: string[], start: Date, end: Date, step: string): Promise { - /** - * Get metrics from prometheus database. - * - * @param dbUrl - prometheus database url - * @param metrics - array of metric names - * @param start - start date - * @param end - end date - * @param step - step size - * @returns csv array - */ - // query format: {__name__=~"metric1|metric2|metric3"} - const url = dbUrl - + '/api/v1/query_range?query=' + encodeURIComponent('{__name__=~"' + metrics.join('|') + '"}') - + '&start=' + encodeURIComponent(start.toISOString()) - + '&end=' + encodeURIComponent(end.toISOString()) - + '&step=' + encodeURIComponent(step); - - // raise error if status is not success - return new Promise((resolve, reject) => { - this.http.get(url).subscribe( - data => { - const parsed_data = this.responseToArray(data); - const blob_data = parsed_data.map(row => row.join(',')).join('\n'); - const dataset = new Dataset( - parsed_data, - new File([blob_data], 'metrics.csv') - ); - resolve(dataset); - }, - error => { - reject(error); - } - ); - }); - } - - private responseToArray(response: any): any[] { - /** - * Convert prometheus response to csv array - * - * @param response - prometheus response - * @returns csv array - */ - const data: MetricType[] = response['data']['result']; - if (!data.length) { - return []; - } - - const [metricNames, metricValues] = this.extractMetricNamesAndValues(data); - this.attachMetricProperties(metricNames, data); - return this.aggregateTimestamps(metricValues, metricNames); - } - - private extractMetricNamesAndValues(data: MetricType[]): [string[], TupleType[][]] { - /** - * Extract metric names and values from the response - * - * @param data - array of metric objects - * @returns array of metric names and array of metric values - */ - const metricNames = data.map(metric => metric.metric['__name__'] as string); - const metricValues: TupleType[][] = data.map(metric => metric.values as TupleType[]); - return [metricNames, metricValues]; - } - - private attachMetricProperties(metricNames: string[], data: MetricType[]): void { - /** - * Attach prometheus metric properties to metric names. - * - * @param metricNames - array of metric names - * @param data - array of metric objects - */ - for (let i = 0; i < metricNames.length; i++) { - const metricLabelNames: string[] = Object.keys(data[i]['metric']).filter(key => key !== '__name__'); - const metricLabelValues: string[] = Object.values(data[i]['metric']).filter(key => key !== metricNames[i]); - metricNames[i] += '_' + this.createMetricLabelPairs(metricLabelNames, metricLabelValues).join('_'); - } - } - - private createMetricLabelPairs(labelNames: string[], labelValues: string[]): string[] { - /** - * Create metric label pairs - * - * @param labelNames - array of label names - * @param labelValues - array of label values - * @returns array of label pairs as strings - */ - return labelNames.map((name, index) => { - const cleanName = name.replace(/[^a-zA-Z0-9]/g, '_'); - const cleanValue = labelValues[index].replace(/[^a-zA-Z0-9]/g, '_'); - return cleanName + "_" + cleanValue; - }); - } - - private aggregateTimestamps(metricValues: TupleType[][], metricNames: string[]): any[] { - /** - * Aggregate timestamps and values into a single array - * [ - * [metric1, metric2, metric3], - * t1 - [value1, value2, value3], - * t2 - [value1, value2, value3], - * t3 - [value1, value2, value3] - * ... - * tn - [value1, value2, value3] - * ] - * - * If a metric does not have a value at a given timestamp, the value is set to 0.0. - * - * @param metricValues - array of metric values - * @param metricNames - array of metric names - * @returns csv array - */ - const timestamps = Array.from(new Set(metricValues.flat().map(v => v[0]))).sort(); - const result = [metricNames]; // First row is metric names, CSV header - - for (const timestamp of timestamps) { - const row = metricValues.map(values => { - const metricValue = values.find(value => value[0] === timestamp); - return metricValue ? metricValue[1] : 0.0; - }); - result.push(row); - } - - return result; - } - - - generateUUID() { - var d = new Date().getTime();//Timestamp - var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported - return 'p' + 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16;//random number between 0 and 16 - if (d > 0) {//Use timestamp until depleted - r = (d + r) % 16 | 0; - d = Math.floor(d / 16); - } else {//Use microseconds since page-load if supported - r = (d2 + r) % 16 | 0; - d2 = Math.floor(d2 / 16); - } - return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); - }); - } + setCredentials(proxyUrl: string, username: string, password: string): void { + this.prometheusService.setCredentials(proxyUrl, username, password); + } + + clearCredentials(): void { + this.prometheusService.clearCredentials(); + } + + setDbUrl(dbUrl: string): Promise<{success: boolean, msg: string}> { + return this.prometheusService.setDbUrl(dbUrl); + } + + getAvailableMetrics(): Promise { + /** + * Get available metrics from prometheus database. + * + * @param dbUrl - prometheus database url + * @returns array of metric names + */ + return this.prometheusService.getAvailableMetrics(); + } + + getMetrics(metrics: string[], start: Date, end: Date, step: string): Promise { + /** + * Get metrics from prometheus database. + * + * @param dbUrl - prometheus database url + * @param metrics - array of metric names + * @param start - start date + * @param end - end date + * @param step - step size + * @returns csv array + */ + return this.prometheusService.getMetrics(metrics, start, end, step); + } + + generateUUID() { + var d = new Date().getTime();//Timestamp + var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;//Time in microseconds since page-load or 0 if unsupported + return 'p' + 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16;//random number between 0 and 16 + if (d > 0) {//Use timestamp until depleted + r = (d + r) % 16 | 0; + d = Math.floor(d / 16); + } else {//Use microseconds since page-load if supported + r = (d2 + r) % 16 | 0; + d2 = Math.floor(d2 / 16); + } + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + } } diff --git a/src/app/core/services/prometheus.service.spec.ts b/src/app/core/services/prometheus.service.spec.ts new file mode 100644 index 0000000..34f0f8c --- /dev/null +++ b/src/app/core/services/prometheus.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { PrometheusService } from './prometheus.service'; + +describe('PrometheusService', () => { + let service: PrometheusService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PrometheusService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/prometheus.service.ts b/src/app/core/services/prometheus.service.ts new file mode 100644 index 0000000..b9d8d40 --- /dev/null +++ b/src/app/core/services/prometheus.service.ts @@ -0,0 +1,251 @@ +import {Injectable} from '@angular/core'; +import {HttpClient, HttpErrorResponse, HttpHeaders} from "@angular/common/http"; +import {Dataset} from "../../shared/models/dataset"; +import {AuthService} from "./auth.service"; +import {Observable} from "rxjs"; + + +interface MetricType { + metric: { + __name__: string; + [key: string]: string; // Other properties of the metric + }; + values: TupleType[]; +} + +type TupleType = [number, any]; // Adjust as per your data structure + + +interface PrometheusResponse { + status: string; + data: string[]; + error: string; +} + + +@Injectable({ + providedIn: 'root' +}) +export class PrometheusService { + + private currentUrl: string = ''; + private proxyUrl: string = ''; + private useCredentials: boolean = false; + + constructor( + private http: HttpClient, + private authService: AuthService, + ) { + } + + private getAuthHeaders(x_target_url: string): HttpHeaders { + const credentials = this.authService.getCredentials(); + return new HttpHeaders({ + 'Authorization': 'Basic ' + credentials, + 'x-target-url': x_target_url + }); + } + + private get(url: string): Observable { + /** + * Prepare request to prometheus database. + * + * If credentials are used, add authorization header to the request. + * + * @param url - prometheus database url + * @returns prometheus response + **/ + + if (this.useCredentials) { + return this.http.get(this.proxyUrl, {headers: this.getAuthHeaders(url)}); + } else { + return this.http.get(url); + } + } + + setCredentials(proxyUrl: string, username: string, password: string): void { + this.useCredentials = true; + this.proxyUrl = proxyUrl; + this.authService.cacheCredentials(username, password); + } + + clearCredentials(): void { + this.useCredentials = false; + this.proxyUrl = ''; + this.authService.clearCredentials(); + } + + setDbUrl(dbUrl: string): Promise<{success: boolean, msg: string}> { + const url = dbUrl + '/api/v1/query?query=up'; + + return new Promise((resolve) => { + this.get(url).subscribe( + data => { + this.currentUrl = dbUrl; + resolve({success: true, msg: 'Successfully connected to the database!'}); + }, + error => { + let msg = 'Failed connection: ' + error.status + ' (' + error.statusText + ') '; + resolve({success: false, msg: msg}); + } + ); + }); + } + + getAvailableMetrics(): Promise { + /** + * Get available metrics from prometheus database. + * + * @param dbUrl - prometheus database url + * @returns array of metric names + */ + const url = this.currentUrl + '/api/v1/label/__name__/values'; + + return new Promise((resolve, reject) => { + this.get(url).subscribe( + data => { + resolve(data['data']); + }, + _ => { + resolve([]); + } + ); + }); + } + + getMetrics(metrics: string[], start: Date, end: Date, step: string): Promise { + /** + * Get metrics from prometheus database. + * + * @param dbUrl - prometheus database url + * @param metrics - array of metric names + * @param start - start date + * @param end - end date + * @param step - step size + * @returns csv array + */ + // query format: {__name__=~"metric1|metric2|metric3"} + const url = this.currentUrl + + '/api/v1/query_range?query=' + encodeURIComponent('{__name__=~"' + metrics.join('|') + '"}') + + '&start=' + encodeURIComponent(start.toISOString()) + + '&end=' + encodeURIComponent(end.toISOString()) + + '&step=' + encodeURIComponent(step); + + // raise error if status is not success + return new Promise((resolve, reject) => { + this.get(url).subscribe( + data => { + const parsed_data = this.responseToArray(data); + const blob_data = parsed_data.map(row => row.join(',')).join('\n'); + + if (!parsed_data.length) { + const error = new HttpErrorResponse( + {error: {message: 'No metrics found!'}} + ); + reject(error); + } + + const dataset = new Dataset( + parsed_data, + new File([blob_data], 'metrics.csv') + ); + resolve(dataset); + }, + error => { + reject(error); + } + ); + }); + } + + private responseToArray(response: any): any[] { + /** + * Convert prometheus response to csv array + * + * @param response - prometheus response + * @returns csv array + */ + const data: MetricType[] = response['data']['result']; + if (!data.length) { + return []; + } + + const [metricNames, metricValues] = this.extractMetricNamesAndValues(data); + this.attachMetricProperties(metricNames, data); + return this.aggregateTimestamps(metricValues, metricNames); + } + + private extractMetricNamesAndValues(data: MetricType[]): [string[], TupleType[][]] { + /** + * Extract metric names and values from the response + * + * @param data - array of metric objects + * @returns array of metric names and array of metric values + */ + const metricNames = data.map(metric => metric.metric['__name__'] as string); + const metricValues: TupleType[][] = data.map(metric => metric.values as TupleType[]); + return [metricNames, metricValues]; + } + + private attachMetricProperties(metricNames: string[], data: MetricType[]): void { + /** + * Attach prometheus metric properties to metric names. + * + * @param metricNames - array of metric names + * @param data - array of metric objects + */ + for (let i = 0; i < metricNames.length; i++) { + const metricLabelNames: string[] = Object.keys(data[i]['metric']).filter(key => key !== '__name__'); + const metricLabelValues: string[] = Object.values(data[i]['metric']).filter(key => key !== metricNames[i]); + metricNames[i] += '_' + this.createMetricLabelPairs(metricLabelNames, metricLabelValues).join('_'); + } + } + + private createMetricLabelPairs(labelNames: string[], labelValues: string[]): string[] { + /** + * Create metric label pairs + * + * @param labelNames - array of label names + * @param labelValues - array of label values + * @returns array of label pairs as strings + */ + return labelNames.map((name, index) => { + const cleanName = name.replace(/[^a-zA-Z0-9]/g, '_'); + const cleanValue = labelValues[index].replace(/[^a-zA-Z0-9]/g, '_'); + return cleanName + "_" + cleanValue; + }); + } + + private aggregateTimestamps(metricValues: TupleType[][], metricNames: string[]): any[] { + /** + * Aggregate timestamps and values into a single array + * [ + * [metric1, metric2, metric3], + * t1 - [value1, value2, value3], + * t2 - [value1, value2, value3], + * t3 - [value1, value2, value3] + * ... + * tn - [value1, value2, value3] + * ] + * + * If a metric does not have a value at a given timestamp, the value is set to 0.0. + * + * @param metricValues - array of metric values + * @param metricNames - array of metric names + * @returns csv array + */ + const timestamps = Array.from(new Set(metricValues.flat().map(v => v[0]))).sort(); + const result = [metricNames]; // First row is metric names, CSV header + + for (const timestamp of timestamps) { + const row = metricValues.map(values => { + const metricValue = values.find(value => value[0] === timestamp); + return metricValue ? metricValue[1] : 0.0; + }); + result.push(row); + } + + return result; + } + +} diff --git a/src/app/shared/components/csv-loader/csv-loader.component.html b/src/app/shared/components/csv-loader/csv-loader.component.html index a738a50..c47a9fc 100644 --- a/src/app/shared/components/csv-loader/csv-loader.component.html +++ b/src/app/shared/components/csv-loader/csv-loader.component.html @@ -33,9 +33,30 @@

Select File

- + + +
+ Provide Credentials +
+ +
+ + + + Username + + + + + + Password + + +
+ @@ -43,6 +64,8 @@

Select File

+ + Choose a metric @@ -84,9 +107,9 @@

Select File

- +
- +
diff --git a/src/app/shared/components/csv-loader/csv-loader.component.ts b/src/app/shared/components/csv-loader/csv-loader.component.ts index 9d0b753..502f584 100644 --- a/src/app/shared/components/csv-loader/csv-loader.component.ts +++ b/src/app/shared/components/csv-loader/csv-loader.component.ts @@ -1,109 +1,135 @@ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { Dataset } from '../../models/dataset'; -import { DataService } from '../../../core/services/data.service'; -import { MatSelectChange } from '@angular/material/select'; +import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {Dataset} from '../../models/dataset'; +import {DataService} from '../../../core/services/data.service'; +import {MatSelectChange} from '@angular/material/select'; +import { trigger, state, style, transition, animate } from '@angular/animations'; + @Component({ - selector: 'app-csv-loader', - templateUrl: './csv-loader.component.html', - styleUrls: ['./csv-loader.component.scss'] + selector: 'app-csv-loader', + templateUrl: './csv-loader.component.html', + styleUrls: ['./csv-loader.component.scss'], + animations: [ + trigger('fadeInOut', [ + state('in', style({ opacity: 1, height: '*' })), + state('out', style({ opacity: 0, height: '0px' })), + transition('out => in', animate('200ms ease-in')), + transition('in => out', animate('200ms ease-out')) + ]) + ] }) export class CsvLoaderComponent implements OnInit { - dataset: Dataset | null = null; - @Output() datasetChange: EventEmitter = new EventEmitter(); - metricDefinitions?: string[]; - - sourceType: 'demo' | 'upload' | 'prometheus' = 'demo'; - assetCsvFiles: string[] = [ - 'chaos-exp-1-trace.csv', - 'chaos-exp-2-trace.csv', - ]; - - dbUrl: string = 'http://localhost:9090'; - dbConnected: boolean = false; - dbMetricLabels: string[] = []; - selectedStartDatetime: Date = new Date(); - selectedEndDatetime: Date = new Date(); - selectedStepSize: string = "1m"; - selectedMetrics: string[] = []; - - constructor(private dataSvc: DataService, private snackBar: MatSnackBar) { - this.selectedStartDatetime.setMinutes(this.selectedStartDatetime.getMinutes() - 30); - } - - ngOnInit(): void { } - - onFileSelected(ev: any) { - const file = ev.srcElement.files[0]; - this.loadCsvFileLocal(file); - } - - onDemoFileChange(event: MatSelectChange) { - const fileName = event.value; - this.loadCsvFileFromAssets(fileName); - } - - async onConnectButtonPressed() { - this.dataSvc.setDbUrl(this.dbUrl).then(res => { - if (res) { - this.dbConnected = true; - this.showSnackbar('Successfully connected to the database!', ['mat-toolbar', 'mat-primary']) - this.loadAvailableMetrics(); - } else { - this.dbConnected = false; - this.showSnackbar('Failed to connect to the database!', ['mat-toolbar', 'mat-warn']) - } - }); - } - - async onQueryButtonPressed() { - await this.dataSvc.getMetrics( - this.dbUrl, - this.selectedMetrics, - this.selectedStartDatetime, - this.selectedEndDatetime, - this.selectedStepSize - ).then(res => { - if (res.length == 0) { - this.showSnackbar('No metrics found!', ['mat-toolbar', 'mat-warn']); - } else { - this.setDataset(res); - } - }).catch(err => { - let msg = err.error.error - this.showSnackbar(msg, ['mat-toolbar', 'mat-warn']); - }) - } - - - async onMetricChange(event: MatSelectChange) { - this.selectedMetrics = event.value; + dataset: Dataset | null = null; + @Output() datasetChange: EventEmitter = new EventEmitter(); + metricDefinitions?: string[]; + + sourceType: 'demo' | 'upload' | 'prometheus' = 'demo'; + assetCsvFiles: string[] = [ + 'chaos-exp-1-trace.csv', + 'chaos-exp-2-trace.csv', + ]; + + dbUrl: string = 'http://localhost:9090'; + dbProxyUrl: string = 'http://localhost:3000/proxy'; + dbUseCredentials: boolean = false; + dbUsername: string = ''; + dbPassword: string = ''; + dbConnected: boolean = false; + dbMetricLabels: string[] = []; + selectedStartDatetime: Date = new Date(); + selectedEndDatetime: Date = new Date(); + selectedStepSize: string = "1m"; + selectedMetrics: string[] = []; + + constructor(private dataSvc: DataService, private snackBar: MatSnackBar) { + this.selectedStartDatetime.setMinutes(this.selectedStartDatetime.getMinutes() - 30); + } + + ngOnInit(): void { + } + + onFileSelected(ev: any) { + const file = ev.srcElement.files[0]; + this.loadCsvFileLocal(file); + } + + onDemoFileChange(event: MatSelectChange) { + const fileName = event.value; + this.loadCsvFileFromAssets(fileName); + } + + async onConnectButtonPressed() { + if (this.dbUseCredentials && (this.dbUsername == '' || this.dbPassword == '' || this.dbProxyUrl == '')) { + this.showSnackbar('Please provide proxy URL and credentials!', ['mat-toolbar', 'mat-warn']) + return; } - async loadCsvFileLocal(file: File) { - const dataset = await this.dataSvc.parseCsvFile(file); - this.setDataset(dataset); - } - - async loadCsvFileFromAssets(fileName: string) { - const dataset = await this.dataSvc.parseCsvFileFromAssets(fileName); - this.setDataset(dataset); - } - - async loadAvailableMetrics() { - this.dbMetricLabels = await this.dataSvc.getAvailableMetrics(this.dbUrl); + if (this.dbUseCredentials) { + this.dataSvc.setCredentials(this.dbProxyUrl, this.dbUsername, this.dbPassword); + } else { + this.dataSvc.clearCredentials(); } - setDataset(dataset: Dataset) { - this.dataset = dataset; - this.datasetChange.emit(this.dataset); - this.metricDefinitions = ['time', ...this.dataset.metricDefinitions]; - } - - private showSnackbar(message: string, panelClass: string[]) { - this.snackBar.open(message, 'Close', {duration: 3000, panelClass: panelClass}); - } + this.dataSvc.setDbUrl(this.dbUrl).then(res => { + if (res["success"]) { + this.dbConnected = true; + this.showSnackbar(res["msg"], ['mat-toolbar', 'mat-primary']) + this.loadAvailableMetrics(); + } else { + this.dbConnected = false; + this.showSnackbar(res["msg"], ['mat-toolbar', 'mat-warn']) + } + }); + } + + async onQueryButtonPressed() { + await this.dataSvc.getMetrics( + this.selectedMetrics, + this.selectedStartDatetime, + this.selectedEndDatetime, + this.selectedStepSize + ).then(res => { + if (res.length == 0) { + this.showSnackbar('No metrics found!', ['mat-toolbar', 'mat-warn']); + } else { + this.setDataset(res); + } + }).catch(err => { + console.log(err); + let msg = err.error.message; + this.showSnackbar(msg, ['mat-toolbar', 'mat-warn']); + }) + } + + + async onMetricChange(event: MatSelectChange) { + this.selectedMetrics = event.value; + } + + async loadCsvFileLocal(file: File) { + const dataset = await this.dataSvc.parseCsvFile(file); + this.setDataset(dataset); + } + + async loadCsvFileFromAssets(fileName: string) { + const dataset = await this.dataSvc.parseCsvFileFromAssets(fileName); + this.setDataset(dataset); + } + + async loadAvailableMetrics() { + this.dbMetricLabels = await this.dataSvc.getAvailableMetrics(); + } + + setDataset(dataset: Dataset) { + this.dataset = dataset; + this.datasetChange.emit(this.dataset); + this.metricDefinitions = ['time', ...this.dataset.metricDefinitions]; + } + + private showSnackbar(message: string, panelClass: string[]) { + this.snackBar.open(message, 'Close', {duration: 3000, panelClass: panelClass}); + } }
{{ column }} {{ val[column] }}