X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?p=it%2Fotf.git;a=blobdiff_plain;f=otf-frontend%2Fclient%2Fsrc%2Fapp%2Flayout%2Fcomponents%2Fstats%2Fstats.service.ts;fp=otf-frontend%2Fclient%2Fsrc%2Fapp%2Flayout%2Fcomponents%2Fstats%2Fstats.service.ts;h=31d872c84c150138ec80795c39cf5c6434852786;hp=0000000000000000000000000000000000000000;hb=14f6f95c84a4a1fa8774190db4a03fd0214ec55f;hpb=f49bd1efeaaddd4891c1f329b18d8cfb28b3e75b diff --git a/otf-frontend/client/src/app/layout/components/stats/stats.service.ts b/otf-frontend/client/src/app/layout/components/stats/stats.service.ts new file mode 100644 index 0000000..31d872c --- /dev/null +++ b/otf-frontend/client/src/app/layout/components/stats/stats.service.ts @@ -0,0 +1,765 @@ +/* Copyright (c) 2019 AT&T Intellectual Property. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +##############################################################################*/ + + +import { Injectable } from '@angular/core'; +import { FeathersService } from 'app/shared/services/feathers.service'; +import { TestExecutionService } from 'app/shared/services/test-execution.service'; +import { Observable, Subject, from } from 'rxjs'; +import { MatDialog } from '@angular/material'; +import { GroupService } from 'app/shared/services/group.service'; +import * as moment from 'moment'; +import { SchedulingService } from 'app/shared/services/scheduling.service'; +import { TestInstanceService } from 'app/shared/services/test-instance.service'; +import { string, number } from '@amcharts/amcharts4/core'; +import { group } from '@angular/animations'; +export interface StatsFilter { + startDate?: Date, + endDate?: Date, + selectedTestDefinitions?: Array, + selectedTestInstances?: Array +} + +@Injectable({ + providedIn: 'root' +}) + +//this service serves as the controller between the dashboard's data management and the components' UI. +export class StatsService { + + //set default filters + public filters: StatsFilter = { + startDate: moment().subtract(1, 'weeks').toDate(), + endDate: moment().toDate() + } + + public executionList: Array = []; + + public testDefinitionData = { + //Executions Array thats made of objects with date, test definition name, and count + "Executions": [], + //Array of Results for the Pie Chart + "Results": [], + } + public testInstanceData = { + //Executions for Each Test Instance + "Executions": [], + //For multilinechart, objects made of test instance names, and execution count for each one. + "Individual_Exec": [] + }; + + //list if test instance names + public testInstances = []; + //list of scheduled tests gotten from agenda in db. + public scheduledTests = []; + + //these are filter objects attached to stats service that are updated whenever user confirms from filter modal. + //They are updated in the filter functions below. + // public tdFilters = { + // startDate: {}, + // endDate: {}, + // selected: [], + // } + // public tiFilters = { + // startDate: {}, + // endDate: {}, + // selectedTDs: [], + // selectedTIs: [], + // //this limit is for the amount of multiple series being displayed on the multiline chart according to default/filters. + // multiLineLimit: 5, + // } + // public scheduleFilters = { + // startDate: {}, + // endDate: {}, + // timeRangeStart: {}, + // timeRangeEnd: {}, + // selectedInstances: [], + // } + + //these are for triggering the listeners located in the chart components. + //executionsChange subjects are triggered when user filters. + public tdExecutionsChange: Subject = new Subject(); + public tiExecutionsChange: Subject = new Subject(); + public scheduleChange: Subject = new Subject(); + public finishedDefaultData: Subject = new Subject(); + public startDefaultData: Subject = new Subject(); + public startTDExecutionCall: Subject = new Subject(); + public startTIExecutionCall: Subject = new Subject(); + public windowResized: Subject = new Subject(); + public startScheduleCall: Subject = new Subject(); + + constructor(public feathers: FeathersService, public testExecution: TestExecutionService, + public _groups: GroupService, public testInstanceService: TestInstanceService, public schedService: SchedulingService) { + + //listening for whether user changes group, if so, variables are reset (rest are inside defaultData, we can work on consistency). + //and we get default data for the new group passed in. + // this.getDefaultData(this._groups.getGroup()); + // this._groups.groupChange().subscribe(group => { + // this.getDefaultData(group); + // }); + + } + + + + //these are trigger functions that we call in the data manipulating functions below. + //the purpose of these functions is to let the listeners set up in the chart components know that the data has been updated. + //then the chart components recall the data using getData(). + checkWindow() { + return this.windowResized; + } + + onDefaultDataCallStarted() { + return this.startDefaultData; + } + + onTDExecutionChangeStarted() { + return this.startTDExecutionCall; + } + + onTIExecutionChangeStarted() { + return this.startTIExecutionCall; + } + + onScheduleChangeStarted() { + return this.startScheduleCall; + } + + onDefaultDataCallFinished() { + return this.finishedDefaultData; + } + + onTDExecutionChangeFinished() { + return this.tdExecutionsChange; + } + + onTIExecutionChangeFinished() { + return this.tiExecutionsChange; + } + + onScheduleChangeFinished() { + return this.scheduleChange; + } + + //one giant getter where we pass in the name of what we want and switch case it. + + //This function is called in the components and returns the relavent array for the component. + getData(name: string) { + let outputData = {}; + + switch (name) { + case "TD_Executions": + return this.testDefinitionData.Executions; + case "TD_Results": + return this.testDefinitionData.Results; + case "testInstances": + return this.testInstanceData.Executions; + case "Schedule": + return this.scheduledTests; + // case "multiLineData": + // //limitting the series being displayed. + // return this.testInstanceData.Individual_Exec.slice(0, this.tiFilters.multiLineLimit); + } + //console.log(outputData); + return outputData; + } + + //this gets called from the filter modal when the user confirms their filters. + filterData(allFilters, tdFilters, tiFilters, schedFilters) { + //this.filterAll(allFilters); + this.filterTDs(tdFilters); + this.filterTIs(tiFilters); + this.filterSchedule(schedFilters) + } + + //this is still under the works, the purpose of this is to filter ALL the components of the dashboard if they have common filtering grounds. + filterAll(allFilters) { + //console.log('Filtering everything') + //console.log(allFilters); + if (allFilters.startDate != "" || allFilters.endDate != "") { + if (allFilters.endDate == "") { + this.testDefinitionData.Executions = this.testDefinitionData.Executions.filter(execution => ( + execution.startTime >= allFilters.startDate + )) + } else if (allFilters.startDate == "") { + this.testDefinitionData.Executions = this.testDefinitionData.Executions.filter(execution => ( + execution.startTime <= allFilters.endDate + )) + } else { + this.testDefinitionData.Executions = this.testDefinitionData.Executions.filter(execution => ( + execution.startTime >= allFilters.startDate && + execution.date <= allFilters.endDate + )) + } + } + } + + /* + this function takes in test definition filters and queries data accordingly. + improvement needed: if the filters provided do not require querying at all, the function should narrow the currently existing data. This + will be faster than requerying in those cases and improve loading times. + */ + async filterTDs(tdFilters) { + + /* + checking if the filters passed in are empty, if so do nothing, if not, trigger a start call that lets the components know querying is going to begin. + these start..Call() functions are so chart components can turn on their loading indicators. + */ + // if (tdFilters.startDate == "" && tdFilters.endDate == "" && tdFilters.selected.length == 0) return; + // else this.startTDExecutionCall.next(tdFilters); + + // //updating filter objects attached to stats service so we can use the service getters to get them where we want in the charts component code. + // this.tdFilters = tdFilters; + + // //if no range is passed in we use the default range of past 2 months. + // let startDate = tdFilters.startDate == "" ? new Date(moment().subtract(2, 'weeks').format('L')) : new Date(tdFilters.startDate); + // let endDate = tdFilters.endDate == "" ? moment().toDate() : new Date(tdFilters.endDate); + // //update service filters accordingly. + // this.tdFilters.startDate = startDate; + // this.tdFilters.endDate = endDate; + //variable of id's of the test definitions selected in the filters. + let selectedIDs = this.filters.selectedTestDefinitions.map(item => item.id); + + //Promise that queries the data according the filters. We use a promise so we wait for the data to query before the components render. + await new Promise((resolve, reject) => { + + //feathers query time. + this.testExecution.find({ + //get the data relevant to the group. + groupId: this._groups.getGroup()["_id"], + //thse are gonna go in the data objects. + $select: [ + 'historicTestDefinition.testName', + 'startTime', + 'testResult' + ], + //UNLIMITED DATA BABY. + $limit: -1, + //sort according to ascending dates. + $sort: { + startTime: 1, + }, + //select the start and end dates from the filter dates. + startTime: { + $gte: this.filters.startDate, + $lte: this.filters.endDate, + }, + //select the test definitions according to the selected ones in the filters. + 'historicTestDefinition._id': { + $in: selectedIDs + } + }).subscribe(result => { + + //console.log(result) + + //resetting real quick cuz why not. + this.testDefinitionData = { + "Executions": [], + "Results": [], + } + + //pretty self explanitory. + let fetchedData = result as Array; + let currentExecutionsData = this.testDefinitionData.Executions; + + /* + for each new fetched json we got with the selected stuff we specified in the feathers query, + we need to organize and distribute them accordingly to our service's data objects. For example, the json objects consist of + 'historicTestDefinition.testName', + 'startTime', + 'testResult', + test results belong in the results array, the rest needs to be organzied in the executions array, + thats what the populate methods are for. + */ + for (let index in fetchedData) { + let newItem = fetchedData[index]; + + //for consistency we're supposed to pass current data to both we'll fix that later, but for now the piechart one just calls the current results data + //inside itself. + this.populateLineChartData(newItem, currentExecutionsData); + this.populatePieChartData(newItem); + } + resolve(); + }) + }).then(res => { + //console.log(res); + + //trigger that querying is done and the test definition executions data has been changed. Line chart and pie chart listen for this. + this.tdExecutionsChange.next(res); + }) + } + + //similar stuffies. just small differences. + async filterTIs(tiFilters) { + + // if (tiFilters.startDate == "" && tiFilters.endDate == "" && tiFilters.selectedTDs.length == 0 && tiFilters.selectedTIs.length == 0) return; + // else this.startTIExecutionCall.next(tiFilters); + + // this.tiFilters = tiFilters; + // if (tiFilters.selectedTIs.length > 0 && tiFilters.selectedTDs.length > 0) this.tiFilters.multiLineLimit = tiFilters.selectedTIs.length + tiFilters.selectedTDs.length; + // else if (tiFilters.selectedTIs.length > 0) this.tiFilters.multiLineLimit = tiFilters.selectedTIs.length; + // else if (tiFilters.selectedTDs.length > 0) this.tiFilters.multiLineLimit = tiFilters.selectedTDs.length; + // else this.tiFilters.multiLineLimit = 5; + + // let startDate = tiFilters.startDate == "" ? new Date(moment().subtract(2, 'weeks').format('L')) : new Date(tiFilters.startDate); + // let endDate = tiFilters.endDate == "" ? moment().toDate() : new Date(tiFilters.endDate); + + // this.tiFilters.startDate = startDate; + // this.tiFilters.endDate = endDate; + // console.log(tiFilters.selectedTDs) + + await new Promise((resolve, reject) => { + this.testExecution.find({ + groupId: this._groups.getGroup()["_id"], + $limit: -1, + startTime: { + $gte: this.filters.startDate, + $lte: this.filters.endDate, + }, + $select: [ + 'startTime', + 'endTime', + 'historicTestDefinition.testName', + 'historicTestInstance.testInstanceName', + ], + $or: [ + { + 'historicTestDefinition._id': { + $in: tiFilters.selectedTDs + } + }, + { + 'historicTestInstance._id': { + $in: tiFilters.selectedTIs + } + } + ] + + }).subscribe(result => { + this.testInstanceData = { + "Executions": [], + "Individual_Exec": [] + } + //console.log(result) + let fetchedData = result as Array; + for (let index in fetchedData) { + let newItem = fetchedData[index]; + this.populateBarChartData(newItem); + this.populateMultiLineChartData(newItem); + } + this.testInstanceData.Executions.sort((a, b) => b.Count - a.Count); + this.testInstanceData.Individual_Exec.sort((a, b) => b.total - a.total); + + resolve(); + }) + }).then(res => { + this.tiExecutionsChange.next(res); + //console.log(this.testInstanceData.Executions); + }) + + } + + //similar stuffies just smol differneces. + async filterSchedule(schedFilters) { + + //console.log(schedFilters); + // this.scheduleFilters = schedFilters; + // //console.log(schedFilters.selectedInstances); + + // if (schedFilters.startDate == "" && + // schedFilters.endDate == "" && + // schedFilters.selectedInstances.length == 0) { + // return; + // } else this.startScheduleCall.next(schedFilters); + + // let startDate = schedFilters.startDate == "" ? new Date(moment().toDate()) : new Date(schedFilters.startDate); + // let endDate = schedFilters.endDate == "" ? new Date(moment().add(2, 'weeks').format('L')) : new Date(schedFilters.endDate); + + // this.scheduleFilters.startDate = startDate; + // this.scheduleFilters.endDate = endDate; + + + this.schedService.find({ + $select: ["data.testSchedule._testInstanceId", 'nextRunAt'], + $limit: -1, + }).subscribe(result => { + this.scheduledTests = []; + //console.log(result); + let fetchedData = result as Array; + let resultingData: Array = fetchedData; + if (schedFilters.selectedInstances.length !== 0) { + resultingData = fetchedData.filter(el => { + let fetchedID = el.data.testSchedule._testInstanceId; + let selectedIDs = schedFilters.selectedInstances as Array; + let condition = selectedIDs.includes(fetchedID.toString()); + //console.log(condition); + return condition; + }) + } + + resultingData = resultingData.filter(el => { + let schedDate = new Date(el.nextRunAt); + return schedDate >= this.filters.startDate && schedDate <= this.filters.endDate; + }) + + for (let index in resultingData) { + let checkIfTestBelongsToUserGroup = this.testInstances.findIndex(testInstances => testInstances.id === resultingData[index].data.testSchedule._testInstanceId); + if (checkIfTestBelongsToUserGroup >= 0) { + if (resultingData[index].nextRunAt) { + let d1 = new Date(resultingData[index].nextRunAt); + this.scheduledTests.push({ + id: resultingData[index].data.testSchedule._testInstanceId, + name: this.testInstances[checkIfTestBelongsToUserGroup].name, + dateExec: d1.toDateString(), + timeExec: d1.toLocaleTimeString() + }) + } + } + } + this.scheduleChange.next(); + }); + + + + } + + //getters for the filter objects. + // getTDFilters() { + // return this.tdFilters; + // } + + // getTIFilters() { + // return this.tiFilters; + // } + + // getSchedFilters() { + // return this.scheduleFilters; + // } + + calcTime(execution) { + var end = new Date(execution.endTime); + var start = new Date(execution.startTime); + var executionTime = (end.getTime() - start.getTime()) / 1000; + return executionTime; + } + + //This function takes an execution that was retrieved from the Database and takes the data it needs for the line chart. + populateLineChartData(execution, currentData) { + let executionDate = new Date(execution.startTime) + + // Looks to see if the date already has an execution./ + let indexOfItemFound = currentData.findIndex((element) => { + + return ( + executionDate.getFullYear() === element.date.getFullYear() && + executionDate.getMonth() === element.date.getMonth() && + executionDate.getDate() === element.date.getDate() + ) + }) + + //If the date is not found. Push a new date into the array with a count of one + if (currentData[indexOfItemFound] == undefined) { + currentData.push({ + date: new Date(executionDate.getFullYear(), executionDate.getMonth(), executionDate.getDate()), + count: 1 + }) + // else update the count + } else currentData[indexOfItemFound].count += 1; + } + + + //Takes an execution and pushes the result/count or updates the count. For the Pie Chart + populatePieChartData(execution) { + + //Check if result is already present in the array. + var checkIfPresent = this.testDefinitionData.Results.find(Results => Results.Name === execution.testResult); + + //If not present, add it to TOPSTATs with a default count of 1. + if (!checkIfPresent) { + + var color; + //Set the color for the pie chart. + if (execution.testResult == "COMPLETED"){ + color = "#0D47A1"; + }else if (execution.testResult == "FAILED") + color = "#DD2C00"; + else if (execution.testResult == "UNKNOWN") + color = "#C4CBD4"; + else if (execution.testResult == "SUCCESS") + color = "#42d660"; + else if (execution.testResult == "success") + color = "#42d660"; + else if (execution.testResult == "STARTED") + color = "#29E3E8"; + else if (execution.testResult == "FAILURE") + color = "#FC9100"; + else if (execution.testResult == "STOPPED") + color = "#900C3F"; + else if (execution.testResult == "TERMINATED") + color = "#AC00FC"; + else if (execution.testResult == "UNAUTHORIZED") + color = "#7A6E6E"; + else if (execution.testResult == "DOES_NOT_EXIST") + color = "#000000"; + else if (execution.testResult == "ERROR") + color = "#eb2acd" + else if (execution.testResult == "WORKFLOW_ERROR") + color = "#194f24" + else + color = "#000000".replace(/0/g, function () { return (~~(Math.random() * 16)).toString(16); }); + + //Push the execution with the count and color. + this.testDefinitionData.Results.push({ Name: execution.testResult, Count: 1, color: color }); + } else { + + //Find index of the testResult and update the count by 1. + var position = this.testDefinitionData.Results.findIndex(Results => Results.Name === execution.testResult); + this.testDefinitionData.Results[position].Count += 1; + } + } + + //Takes an execution and pushes result into the barchart. + populateBarChartData(execution) { + + //check if test instance is present in the array. + var checkIfPresent = this.testInstanceData.Executions.find(Instances => Instances.id === execution.historicTestInstance._id); + + //calculates the time it took for the execution/ + var executionTime = this.calcTime(execution); + + //If execution is not present, push the test instance with a count of 1. + if (!checkIfPresent) { + //If not present, add it to testInstanceData with a default count of 1. + + this.testInstanceData.Executions.push({ + Name: execution.historicTestInstance.testInstanceName, + id: execution.historicTestInstance._id, + testResult: execution.testResult, + executionTime: executionTime, + Count: 1, + Average: executionTime + }); + } else { + // If Present update count and execution time. + var position = this.testInstanceData.Executions.findIndex(Instances => Instances.id === execution.historicTestInstance._id); + this.testInstanceData.Executions[position].Count += 1; + this.testInstanceData.Executions[position].executionTime += executionTime; + this.testInstanceData.Executions[position].Average = this.testInstanceData.Executions[position].executionTime / this.testInstanceData.Executions[position].Count; + } + + } + + //queries data for the scheduled tests. + getScheduledTests(groupId) { + + //Queries a list of test instances by group ID + this.testInstanceService.find({ + groupId: groupId['_id'], + $select: ["_id", "testInstanceName", "groupId"], + $limit: -1 + }).subscribe(result => { + + //Iterate through the list and add the test instances to the list. + for (var index in result) { + var checkIfPresent = this.testInstances.find(id => id === result[index]._id); + if (!checkIfPresent) + this.testInstances.push({ id: result[index]._id, name: result[index].testInstanceName }); + } + }); + + //Queries all of the scheduled tests. + this.schedService.find({ + + $select: ["data.testSchedule._testInstanceId", 'nextRunAt'], + $limit: -1, + $sort: { + startTime: 1 + }, + + }).subscribe(result => { + + this.scheduledTests = []; + for (var index in result) { + + //If the scheduled testinstance is owned by the group, push the result. + var checkIfTestBelongsToUserGroup = this.testInstances.findIndex(testInstances => testInstances.id === result[index].data.testSchedule._testInstanceId); + if (checkIfTestBelongsToUserGroup >= 0) { + + //If the next run at is valid, the test is scheduled. + if (result[index].nextRunAt) { + let d1 = new Date(result[index].nextRunAt); + this.scheduledTests.push({ + id: result[index].data.testSchedule._testInstanceId, + name: this.testInstances[checkIfTestBelongsToUserGroup].name, + dateExec: d1.toDateString(), + timeExec: d1.toLocaleTimeString() + }); + } + } + } + }); + } + + //populate multi line chart + populateMultiLineChartData(execution) { + + let executionDate = new Date(execution.startTime) + let currentData = this.testInstanceData.Individual_Exec; + let count = 1; + //find if Instance is already present in the array. + let position = this.testInstanceData.Individual_Exec.findIndex(Instances => Instances.id === execution.historicTestInstance._id); + + //First execution for this instance + if (currentData[position] == undefined) { + currentData.push({ + testInstanceName: execution.historicTestInstance.testInstanceName, + testDefinitionName: execution.historicTestDefinition.testDefintionName, + id: execution.historicTestInstance._id, + dateData: [{ date: executionDate, count: count, name: execution.historicTestInstance.testInstanceName }], + total: 1 + }) + //execution already present + } else { + //find index of Date + let indexOfDate = currentData[position].dateData.findIndex((element) => { + return ( + executionDate.getFullYear() === element.date.getFullYear() && + executionDate.getMonth() === element.date.getMonth() && + executionDate.getDate() === element.date.getDate() + ) + }); + + //Check if the exeuction date is valid for this instance. If it is not present, push a new date and count. + if (currentData[position].dateData[indexOfDate] == undefined) { + let count = 1; + //Push the new Date + currentData[position].dateData.push({ date: executionDate, count: count, name: execution.historicTestInstance.testInstanceName, id: execution.historicTestInstance._id}); + currentData[position].total++; + } else { + //date is already present + currentData[position].dateData[indexOfDate].count++; + currentData[position].total++; + } + } + } + //Gets the initial data for the default page. + async getDefaultData(group, query?) { + if(!group){ + return; + } + + this.scheduledTests = []; + + this.startDefaultData.next(group); + let groupId = group; + //let startDate = moment().subtract(2, 'weeks').toDate(); + + //query sheduled tests + //this.getScheduledTests(group); + + if(!query){ + query = { + groupId: groupId['_id'], + $select: [ + 'startTime', + 'endTime', + "historicTestDefinition._id", + "historicTestDefinition.testName", + "historicTestInstance._id", + "historicTestInstance.testInstanceName", + "testHeadResults.startTime", + "testHeadResults.endTime", + "testHeadResults.testHeadName", + "testHeadResults.testHeadId", + "testHeadResults.testHeadGroupId", + "testHeadResults.statusCode", + 'testResult' + ], + $limit: -1, + $sort: { + startTime: 1 + }, + startTime: { + $gte: this.filters.startDate, + $lte: this.filters.endDate + } + } + } + + //Query test Executions + await new Promise((resolve, reject) => { + this.testExecution.find(query).subscribe(result => { + + //reset arrays + this.testDefinitionData = { + //Executions Array thats made of objects with date, tdName, result + "Executions": [], + "Results": [], + } + this.testInstanceData = { + "Executions": [], + "Individual_Exec": [] + }; + + this.executionList = result as Array; + let currentData = this.testDefinitionData.Executions; + + + + //iterate through the results and populate the appropriate arrays. + for (let index in this.executionList) { + + let newItem = this.executionList[index]; + + //get default line chart Data + this.populateLineChartData(newItem, currentData); + + //get pie chart data. + this.populatePieChartData(newItem); + + //Get BarChart Data + //this.populateBarChartData(newItem); + + //get multi line chart data + //this.populateMultiLineChartData(newItem); + } + + //sort the two arrays descending. + this.testInstanceData.Executions.sort((a, b) => b.Count - a.Count); + this.testInstanceData.Individual_Exec.sort((a, b) => b.total - a.total); + resolve(); + }, err => { + reject(err); + }); + }).then(res => { + + // this.tdFilters = { + // startDate: moment().subtract(2, 'weeks').toDate(), + // endDate: moment().toDate(), + // selected: [], + // }; + // this.tiFilters = { + // startDate: moment().subtract(2, 'weeks').toDate(), + // endDate: moment().toDate(), + // selectedTDs: [], + // selectedTIs: [], + // multiLineLimit: 5, + // } + this.finishedDefaultData.next(res); + }) + } + + +}