From 34efaa5c6d0879b7dcbb333d46d34bc2e943b1a8 Mon Sep 17 00:00:00 2001 From: mario meltzow Date: Wed, 21 Aug 2024 09:22:14 +0200 Subject: [PATCH] support multiple datasources --- .../cycle-time-scatterplot.component.ts | 3 +- .../datasource/datasource-edit.component.ts | 8 +- src/app/components/layout/layout.component.ts | 14 +- .../monte-carlo/monte-carlo-page.component.ts | 3 +- .../status-history-table.component.ts | 3 +- .../status-mapping.component.ts | 13 +- .../throughput-page.component.ts | 3 +- .../work-in-progress-page.component.ts | 3 +- .../work-item-age-page/work-item-age.page.ts | 5 +- src/app/models/canceledCycleEntry.ts | 1 + src/app/models/cycleTimeEntry.ts | 1 + src/app/models/datasource.ts | 4 +- src/app/models/issue.ts | 2 +- src/app/models/status.ts | 2 +- src/app/models/throughputEntry.ts | 1 + src/app/models/workInProgressEntry.ts | 1 + src/app/models/workItemAgeEntry.ts | 1 + .../services/business-logic.service.spec.ts | 112 ++------------- src/app/services/business-logic.service.ts | 55 +++----- src/app/services/jira-cloud.service.ts | 12 +- src/app/services/jira-data-center.service.ts | 6 +- src/app/services/storage.service.ts | 129 ++++++++++-------- 22 files changed, 154 insertions(+), 228 deletions(-) diff --git a/src/app/components/cycle-time-scatterplot/cycle-time-scatterplot.component.ts b/src/app/components/cycle-time-scatterplot/cycle-time-scatterplot.component.ts index 5c8654f..5b1fa29 100644 --- a/src/app/components/cycle-time-scatterplot/cycle-time-scatterplot.component.ts +++ b/src/app/components/cycle-time-scatterplot/cycle-time-scatterplot.component.ts @@ -98,7 +98,8 @@ export class CycleTimeScatterplotComponent { } async loadData() { - const items = await this.databaseService.getCycleTimeData(); + const appSettings = await this.databaseService.getAppSettings(); + const items = await this.databaseService.getCycleTimeData(appSettings.selectedDatasourceId); items.sort((a, b) => a.issueId > b.issueId ? 1 : -1); // Sort items by issueId in descending order this.percentilValue = this.businessLogicService.computePercentile(items.map(item => item.cycleTime), 80); this.updateAnnotationLine(this.percentilValue); diff --git a/src/app/components/datasource/datasource-edit.component.ts b/src/app/components/datasource/datasource-edit.component.ts index 3e81333..324f042 100644 --- a/src/app/components/datasource/datasource-edit.component.ts +++ b/src/app/components/datasource/datasource-edit.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import { StorageService } from '../../services/storage.service'; -import {Datasource, DataSourceType} from '../../models/datasource'; +import {Datasource, DatasourceType} from '../../models/datasource'; import {MatGridList, MatGridTile} from "@angular/material/grid-list"; import {MatError, MatFormField, MatLabel} from "@angular/material/form-field"; import {MatInput} from "@angular/material/input"; @@ -36,7 +36,7 @@ import {StatusMappingComponent} from "../status-mapping/status-mapping.component export class DatasourceEditComponent implements OnInit { datasourceForm: FormGroup; datasourceId?: number; - datasourceTypes = Object.values(DataSourceType); + datasourceTypes = Object.values(DatasourceType); currentStates: Status[] = []; constructor( @@ -64,7 +64,7 @@ export class DatasourceEditComponent implements OnInit { jql: dataset.jql ?? '', type: dataset.type ?? this.datasourceTypes[0] }); - this.currentStates = await this.storageService.getAllStatuses(); + this.currentStates = await this.storageService.getAllStatuses(this.datasourceId); } } @@ -78,7 +78,7 @@ export class DatasourceEditComponent implements OnInit { await this.storageService.updateDatasource(updatedDataset); this.toastr.success('Datasource updated'); } else { - await this.storageService.createDataset(updatedDataset); + await this.storageService.createDatasource(updatedDataset); this.toastr.success('Datasource created'); } this.router.navigate([DATASOURCE_LIST]); diff --git a/src/app/components/layout/layout.component.ts b/src/app/components/layout/layout.component.ts index 4b1f993..a755335 100644 --- a/src/app/components/layout/layout.component.ts +++ b/src/app/components/layout/layout.component.ts @@ -13,7 +13,7 @@ import {RouterLink, RouterOutlet} from "@angular/router"; import {MatSelectModule} from "@angular/material/select"; import {MatFormFieldModule} from "@angular/material/form-field"; import {StorageService} from "../../services/storage.service"; -import {Datasource, DataSourceType} from "../../models/datasource"; +import {Datasource, DatasourceType} from "../../models/datasource"; import {ToastrService} from "ngx-toastr"; import { CYCLE_TIME, @@ -93,10 +93,10 @@ export class LayoutComponent implements OnInit { async login() { if (this.selectedDatasource) { - const selectedDataset = this.datasources.find(dataset => dataset.id === this.selectedDatasource); + const selectedDatasource = this.datasources.find(dataset => dataset.id === this.selectedDatasource); await this.storageService.saveAppSettings({selectedDatasourceId: this.selectedDatasource}); - if (selectedDataset) { - if (selectedDataset.type === 'JIRA_CLOUD') { + if (selectedDatasource) { + if (selectedDatasource.type === 'JIRA_CLOUD') { this.loginToJiraCloud(); } else { this.loginToJiraDataCenter(); @@ -124,9 +124,9 @@ export class LayoutComponent implements OnInit { async refreshIssues(silent = false) { if (this.selectedDatasource) { - const selectedDataset = this.datasources.find(dataset => dataset.id === this.selectedDatasource); - if (selectedDataset) { - if (selectedDataset.type === DataSourceType.JIRA_CLOUD) { + const selectedDatasource = this.datasources.find(dataset => dataset.id === this.selectedDatasource); + if (selectedDatasource) { + if (selectedDatasource.type === DatasourceType.JIRA_CLOUD) { await this.jiraCloudService.getAndSaveIssues(this.selectedDatasource!); try { diff --git a/src/app/components/monte-carlo/monte-carlo-page.component.ts b/src/app/components/monte-carlo/monte-carlo-page.component.ts index 94415c5..53f3106 100644 --- a/src/app/components/monte-carlo/monte-carlo-page.component.ts +++ b/src/app/components/monte-carlo/monte-carlo-page.component.ts @@ -85,7 +85,8 @@ export class MonteCarloPageComponent implements OnInit { } private async getThroughputsFromLast20Days(): Promise { - const throughputEntries = await this.storageService.getThroughputData(); + const appSettings = await this.storageService.getAppSettings(); + const throughputEntries = await this.storageService.getThroughputData(appSettings.selectedDatasourceId); const today = new Date(); const last20DaysThroughputs: number[] = []; diff --git a/src/app/components/status-history-table/status-history-table.component.ts b/src/app/components/status-history-table/status-history-table.component.ts index 29db278..6a51c9d 100644 --- a/src/app/components/status-history-table/status-history-table.component.ts +++ b/src/app/components/status-history-table/status-history-table.component.ts @@ -63,7 +63,8 @@ export class StatusHistoryTableComponent implements OnInit { } async ngOnInit() { - const issueHistories = await this.storageService.getAllIssueHistoriesForStatuses(); + const appSettings = await this.storageService.getAppSettings(); + const issueHistories = await this.storageService.getAllIssueHistoriesForStatuses(appSettings.selectedDatasourceId); const issuesIds = this.businessLogic.removeDuplicates(issueHistories.map(issueHistory => issueHistory.issueId), (a, b) => a == b); const issues = await this.storageService.getIssuesByIds(issuesIds); diff --git a/src/app/components/status-mapping/status-mapping.component.ts b/src/app/components/status-mapping/status-mapping.component.ts index f947e9c..d8e947b 100644 --- a/src/app/components/status-mapping/status-mapping.component.ts +++ b/src/app/components/status-mapping/status-mapping.component.ts @@ -5,6 +5,7 @@ import {StorageService} from '../../services/storage.service'; import {NgForOf} from "@angular/common"; import {MatChip} from "@angular/material/chips"; import {ToastrService} from "ngx-toastr"; +import {ActivatedRoute, Router} from "@angular/router"; @Component({ selector: 'app-status-mapping', @@ -22,13 +23,19 @@ export class StatusMappingComponent implements OnInit { statuses: Status[] = []; statusCategories = Object.values(StatusCategory); connectedDropLists: string[] = []; + datasourceId?: number; - constructor(private storageService: StorageService, private toastr: ToastrService) { + constructor(private storageService: StorageService, private toastr: ToastrService, private route: ActivatedRoute, + private router: Router,) { } async ngOnInit() { - this.statuses = await this.storageService.getAllStatuses(); - this.connectedDropLists = ['uncategorized', ...this.statusCategories.map(category => category)]; + this.datasourceId = +this.route.snapshot.paramMap.get('id')!; + if (this.datasourceId !== undefined && this.datasourceId > 0) { + this.statuses = await this.storageService.getAllStatuses(this.datasourceId); + this.connectedDropLists = ['uncategorized', ...this.statusCategories.map(category => category)]; + } + } async drop(event: CdkDragDrop, categoryName: string) { diff --git a/src/app/components/throughput-page/throughput-page.component.ts b/src/app/components/throughput-page/throughput-page.component.ts index 995d34a..939c6e4 100644 --- a/src/app/components/throughput-page/throughput-page.component.ts +++ b/src/app/components/throughput-page/throughput-page.component.ts @@ -42,7 +42,8 @@ export class ThroughputPageComponent implements OnInit { constructor(private storageService: StorageService) {} async ngOnInit() { - const throughputData: ThroughputEntry[] = await this.storageService.getThroughputData(); + const appSettings = await this.storageService.getAppSettings(); + const throughputData: ThroughputEntry[] = await this.storageService.getThroughputData(appSettings.selectedDatasourceId); const allDates = this.generateAllDates(throughputData); const mappedData = this.mapDataToDates(allDates, throughputData); diff --git a/src/app/components/work-in-progress/work-in-progress-page.component.ts b/src/app/components/work-in-progress/work-in-progress-page.component.ts index d437177..87fc751 100644 --- a/src/app/components/work-in-progress/work-in-progress-page.component.ts +++ b/src/app/components/work-in-progress/work-in-progress-page.component.ts @@ -43,7 +43,8 @@ export class WorkInProgressPageComponent implements OnInit { } async ngOnInit() { - const wipData: WorkInProgressEntry[] = await this.storageService.getWorkInProgressData(); + const appSettings = await this.storageService.getAppSettings(); + const wipData: WorkInProgressEntry[] = await this.storageService.getWorkInProgressData(appSettings.selectedDatasourceId); const allDates = this.generateAllDates(wipData); const mappedData = this.mapDataToDates(allDates, wipData); diff --git a/src/app/components/work-item-age-page/work-item-age.page.ts b/src/app/components/work-item-age-page/work-item-age.page.ts index a87201b..04e5418 100644 --- a/src/app/components/work-item-age-page/work-item-age.page.ts +++ b/src/app/components/work-item-age-page/work-item-age.page.ts @@ -81,9 +81,10 @@ export class WorkItemAgePage implements OnInit { } async loadData() { - let data = await this.databaseService.getWorkItemAgeData(); + const appSettings = await this.databaseService.getAppSettings(); + let data = await this.databaseService.getWorkItemAgeData(appSettings.selectedDatasourceId); - const statusInProgress = await this.databaseService.getAllStatuses().then(statuses => { + const statusInProgress = await this.databaseService.getAllStatuses(appSettings.selectedDatasourceId).then(statuses => { return statuses.filter(status => status.category === StatusCategory.InProgress); }) diff --git a/src/app/models/canceledCycleEntry.ts b/src/app/models/canceledCycleEntry.ts index 481ad59..0637b52 100644 --- a/src/app/models/canceledCycleEntry.ts +++ b/src/app/models/canceledCycleEntry.ts @@ -1,6 +1,7 @@ // a cycle is started if the issue switches into a inProgressState from a state that is not inProgressState // because of this, it's possible that some issues has multiple cycles export interface CanceledCycleEntry { + datasourceId: number; id?: number; issueId: number; issueKey: string; diff --git a/src/app/models/cycleTimeEntry.ts b/src/app/models/cycleTimeEntry.ts index 2d69a9d..644a249 100644 --- a/src/app/models/cycleTimeEntry.ts +++ b/src/app/models/cycleTimeEntry.ts @@ -1,6 +1,7 @@ // a cycle is started if the issue switches into a inProgressState from a state that is not inProgressState // because of this, it's possible that some issues has multiple cycles export interface CycleTimeEntry { + datasourceId: number; id?: number; issueId: number; issueKey: string; diff --git a/src/app/models/datasource.ts b/src/app/models/datasource.ts index c0ad400..eef6a01 100644 --- a/src/app/models/datasource.ts +++ b/src/app/models/datasource.ts @@ -1,11 +1,11 @@ -export enum DataSourceType { +export enum DatasourceType { JIRA_CLOUD = 'JIRA_CLOUD', JIRA_DATACENTER = 'JIRA_DATACENTER', } //FIXME: rename this to datasource export interface Datasource { - type: DataSourceType; + type: DatasourceType; baseUrl: string; access_token: string; cloudId?: string; diff --git a/src/app/models/issue.ts b/src/app/models/issue.ts index 9f17d71..5b78053 100644 --- a/src/app/models/issue.ts +++ b/src/app/models/issue.ts @@ -1,5 +1,5 @@ export interface Issue { - dataSourceId: number; + datasourceId: number; issueKey: string; title: string; createdDate: Date; diff --git a/src/app/models/status.ts b/src/app/models/status.ts index 6a170f1..7bcb51f 100644 --- a/src/app/models/status.ts +++ b/src/app/models/status.ts @@ -5,7 +5,7 @@ export enum StatusCategory { } export interface Status { - dataSourceId: number; + datasourceId: number; id?: number; name: string; color?: string; diff --git a/src/app/models/throughputEntry.ts b/src/app/models/throughputEntry.ts index 244bf2f..8c6fb16 100644 --- a/src/app/models/throughputEntry.ts +++ b/src/app/models/throughputEntry.ts @@ -1,4 +1,5 @@ export interface ThroughputEntry { + datasourceId: number; issueIds: number[]; throughput: number; date: Date; diff --git a/src/app/models/workInProgressEntry.ts b/src/app/models/workInProgressEntry.ts index 1275e82..94cb92d 100644 --- a/src/app/models/workInProgressEntry.ts +++ b/src/app/models/workInProgressEntry.ts @@ -1,4 +1,5 @@ export interface WorkInProgressEntry { + datasourceId: number; issueIds: number[]; wip: number; date: Date; diff --git a/src/app/models/workItemAgeEntry.ts b/src/app/models/workItemAgeEntry.ts index 708efeb..00208e4 100644 --- a/src/app/models/workItemAgeEntry.ts +++ b/src/app/models/workItemAgeEntry.ts @@ -1,5 +1,6 @@ export interface WorkItemAgeEntry { id?: number; + datasourceId: number; issueId: number; issueKey: string; title: string; diff --git a/src/app/services/business-logic.service.spec.ts b/src/app/services/business-logic.service.spec.ts index 2fca465..e8cd6a9 100644 --- a/src/app/services/business-logic.service.spec.ts +++ b/src/app/services/business-logic.service.spec.ts @@ -25,99 +25,12 @@ describe('BusinessLogicService', () => { storageService = TestBed.inject(StorageService) as jasmine.SpyObj; }); - it('should return the latest resolved issue history', async () => { - const issueHistories: IssueHistory[] = [ - {field: 'status', toValueId: 1, createdDate: new Date('2023-01-01')}, - {field: 'status', toValueId: 2, createdDate: new Date('2023-02-01')} - ] as IssueHistory[]; - - const statuses: Status[] = [ - { - externalId: 1, category: StatusCategory.Done, - dataSourceId: 0, - name: 'resolved' - }, - { - externalId: 2, category: StatusCategory.Done, - dataSourceId: 0, - name: 'wont do' - } - ]; - - storageService.getAllStatuses.and.returnValue(Promise.resolve(statuses)); - - const result = await service.findLatestResolvedIssueHistory(issueHistories); - expect(result!.id).toEqual(issueHistories[1].id); - }); - - it('should return the first in-progress issue history', async () => { - const issueHistories: IssueHistory[] = [ - {field: 'status', toValueId: 1, createdDate: new Date('2023-01-01')}, - {field: 'status', toValueId: 2, createdDate: new Date('2023-02-01')} - ] as IssueHistory[]; - - const statuses: Status[] = [ - { - externalId: 1, category: StatusCategory.InProgress, - dataSourceId: 0, - name: 'In Arbeit' - }, - { - externalId: 2, category: StatusCategory.InProgress, - dataSourceId: 0, - name: 'in review' - } - ]; - - storageService.getAllStatuses.and.returnValue(Promise.resolve(statuses)); - - const result = await service.findFirstInProgressIssueHistory(issueHistories); - expect(result).toEqual(issueHistories[1]); - }); - - it('but If the issue was already in done, It should return the first in-progress issue history after reopen the issue', async () => { - const issueHistories: IssueHistory[] = [ - {field: 'status', toValueId: 1, createdDate: new Date('2023-01-01'), id: 1}, - {field: 'status', toValueId: 2, createdDate: new Date('2023-02-01'), id: 2}, - {field: 'status', toValueId: 3, createdDate: new Date('2023-02-01'), id: 3}, - //reopen the issue - {field: 'status', toValueId: 1, createdDate: new Date('2023-03-01'), id: 4}, - {field: 'status', toValueId: 2, createdDate: new Date('2023-04-01'), id: 5}, - ] as IssueHistory[]; - - const statuses: Status[] = [ - { - externalId: 2, category: StatusCategory.InProgress, - dataSourceId: 0, - name: 'in review', - order: 2 // the second in-progress status - }, - { - externalId: 1, category: StatusCategory.InProgress, - dataSourceId: 0, - name: 'In Arbeit', - order: 1 // the first in-progress status - }, - { - externalId: 3, category: StatusCategory.Done, - dataSourceId: 0, - name: 'Done', - order: 3 - } - ]; - - storageService.getAllStatuses.and.returnValue(Promise.resolve(statuses)); - - const result = await service.findFirstInProgressIssueHistory(issueHistories); - expect(result).toEqual(issueHistories[4]); - }); - it('should map issue histories to cycle time entries', async () => { const issue: Issue = { id: 1, issueKey: 'ISSUE-1', title: 'Test Issue', - dataSourceId: 1, + datasourceId: 1, createdDate: new Date('2023-01-01'), status: 'Done', externalStatusId: 3, @@ -148,9 +61,9 @@ describe('BusinessLogicService', () => { ]; const statuses: Status[] = [ - {externalId: 1, category: StatusCategory.ToDo, dataSourceId: 1, name: 'To Do'}, - {externalId: 2, category: StatusCategory.InProgress, dataSourceId: 1, name: 'In Progress'}, - {externalId: 3, category: StatusCategory.Done, dataSourceId: 1, name: 'Done'} + {externalId: 1, category: StatusCategory.ToDo, datasourceId: 1, name: 'To Do'}, + {externalId: 2, category: StatusCategory.InProgress, datasourceId: 1, name: 'In Progress'}, + {externalId: 3, category: StatusCategory.Done, datasourceId: 1, name: 'Done'} ]; storageService.getAllStatuses.and.returnValue(Promise.resolve(statuses)); @@ -170,7 +83,8 @@ describe('BusinessLogicService', () => { status: 'Done', externalStatusId: 3, externalResolvedStatusId: 3, - externalInProgressStatusId: 2 + externalInProgressStatusId: 2, + datasourceId: 1 } ]; @@ -182,7 +96,7 @@ describe('BusinessLogicService', () => { id: 1, issueKey: 'ISSUE-1', title: 'Test Issue', - dataSourceId: 1, + datasourceId: 1, createdDate: new Date('2023-01-01'), status: 'Done', externalStatusId: 3, @@ -233,9 +147,9 @@ describe('BusinessLogicService', () => { ]; const statuses: Status[] = [ - {externalId: 1, category: StatusCategory.ToDo, dataSourceId: 1, name: 'To Do'}, - {externalId: 2, category: StatusCategory.InProgress, dataSourceId: 1, name: 'In Progress'}, - {externalId: 3, category: StatusCategory.Done, dataSourceId: 1, name: 'Done'} + {externalId: 1, category: StatusCategory.ToDo, datasourceId: 1, name: 'To Do'}, + {externalId: 2, category: StatusCategory.InProgress, datasourceId: 1, name: 'In Progress'}, + {externalId: 3, category: StatusCategory.Done, datasourceId: 1, name: 'Done'} ]; storageService.getAllStatuses.and.returnValue(Promise.resolve(statuses)); @@ -255,7 +169,8 @@ describe('BusinessLogicService', () => { status: 'Done', externalStatusId: 3, externalResolvedStatusId: 3, - externalInProgressStatusId: 2 + externalInProgressStatusId: 2, + datasourceId: 1 }, { inProgressState: 'In Progress', @@ -269,7 +184,8 @@ describe('BusinessLogicService', () => { status: 'Done', externalStatusId: 3, externalResolvedStatusId: 3, - externalInProgressStatusId: 2 + externalInProgressStatusId: 2, + datasourceId: 1 } ]; diff --git a/src/app/services/business-logic.service.ts b/src/app/services/business-logic.service.ts index da6975b..c443204 100644 --- a/src/app/services/business-logic.service.ts +++ b/src/app/services/business-logic.service.ts @@ -35,7 +35,7 @@ export class BusinessLogicService { // Add a separate IssueHistory entry for the issue with createdDate const issueCreatedHistory: IssueHistory = { issueId: issue.id!, - datasourceId: issue.dataSourceId, + datasourceId: issue.datasourceId, fromValue: '', toValueId: Number.parseInt(findStatusHistory.items[0].from), toValue: firstStatusChange.items[0].fromString, @@ -55,7 +55,7 @@ export class BusinessLogicService { }) => { const issueHistory: IssueHistory = { issueId: issue.id!, - datasourceId: issue.dataSourceId, + datasourceId: issue.datasourceId, fromValue: item.fromString || '', fromValueId: Number.parseInt(item.from), toValueId: Number.parseInt(item.to), @@ -69,13 +69,13 @@ export class BusinessLogicService { return issueHistories; } - async getAllInProgressStatuses(): Promise { - const allStatuses = await this.storageService.getAllStatuses(); + async getAllInProgressStatuses(dataSourceId: number): Promise { + const allStatuses = await this.storageService.getAllStatuses(dataSourceId); return allStatuses.filter(status => status.category === StatusCategory.InProgress); } public async findFirstInProgressStatusChange(issue: Issue) { - const allStatuses = await this.storageService.getAllStatuses(); + const allStatuses = await this.storageService.getAllStatuses(issue.datasourceId); const inProgressStatusIds = allStatuses .filter(status => status.category === StatusCategory.InProgress) .map(status => status.externalId); @@ -93,38 +93,12 @@ export class BusinessLogicService { } - public async findLatestResolvedIssueHistory(issueHistories: IssueHistory[]): Promise { - const allStatuses = await this.storageService.getAllStatuses(); - const doneStatusIds = allStatuses - .filter(status => status.category === StatusCategory.Done) - .map(status => status.externalId); - - const resolvedHistories = issueHistories - .filter(history => history.field === 'status' && doneStatusIds.includes(history.toValueId!)) - .sort((a, b) => new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime()); - - return resolvedHistories.length > 0 ? resolvedHistories[0] : undefined; - } - - public async findFirstInProgressIssueHistory(issueHistories: IssueHistory[]): Promise { - const allStatuses = await this.storageService.getAllStatuses(); - const doneStatusIds = allStatuses - .filter(status => status.category === StatusCategory.InProgress) - .map(status => status.externalId); - - const resolvedHistories = issueHistories - .filter(history => history.field === 'status' && doneStatusIds.includes(history.toValueId!)) - .sort((a, b) => new Date(b.createdDate).getTime() - new Date(a.createdDate).getTime()); - - return resolvedHistories.length > 0 ? resolvedHistories[0] : undefined; - } - public findAllNewStatuses(issues: Issue[], issueHistories: IssueHistory[]): Status[] { let statuses = new Array(); issues.forEach(issue => { const status: Status = { - dataSourceId: issue.dataSourceId, + datasourceId: issue.datasourceId, name: issue.status, externalId: issue.externalStatusId }; @@ -137,12 +111,12 @@ export class BusinessLogicService { if (history.fromValueId && !this.stateExistsInSet(statuses, history.fromValueId!)) { statuses.push({ - dataSourceId: history.datasourceId, + datasourceId: history.datasourceId, name: history.fromValue, externalId: history.fromValueId! }); } else if (history.toValueId && !this.stateExistsInSet(statuses, history.toValueId!)) { - statuses.push({dataSourceId: history.datasourceId, name: history.toValue, externalId: history.toValueId!}); + statuses.push({datasourceId: history.datasourceId, name: history.toValue, externalId: history.toValueId!}); } } }); @@ -196,7 +170,7 @@ export class BusinessLogicService { const canceledCycleEntries: CanceledCycleEntry[] = []; let workItemAgeEntry: WorkItemAgeEntry | null = null; - const allStatuses = await this.storageService.getAllStatuses(); + const allStatuses = await this.storageService.getAllStatuses(i.datasourceId); const inProgressStatusIds = allStatuses.filter(status => status.category === StatusCategory.InProgress).map(status => status.externalId); const doneStatusIds = allStatuses.filter(status => status.category === StatusCategory.Done).map(status => status.externalId); const toDoProgressStatusIds = allStatuses.filter(status => status.category === StatusCategory.ToDo).map(status => status.externalId); @@ -222,6 +196,7 @@ export class BusinessLogicService { if (startCycleEntry && endCycleEntry) { // End the cycle if the issue switches into a Done state const cycleTimeEntry: CycleTimeEntry = { + datasourceId: i.datasourceId, status: endCycleEntry.toValue, externalStatusId: endCycleEntry.toValueId!, inProgressState: startCycleEntry.toValue, @@ -241,6 +216,7 @@ export class BusinessLogicService { } else if (startCycleEntry && canceledCycleEntry) { //create a canceled cycle entry const canceledCycle: CanceledCycleEntry = { + datasourceId: i.datasourceId, status: canceledCycleEntry.toValue, externalStatusId: canceledCycleEntry.toValueId!, inProgressState: startCycleEntry.toValue, @@ -261,6 +237,7 @@ export class BusinessLogicService { } if (startCycleEntry) { workItemAgeEntry = { + datasourceId: i.datasourceId, status: i.status, externalStatusId: i.externalStatusId, inProgressState: startCycleEntry.toValue, @@ -297,6 +274,7 @@ export class BusinessLogicService { throughputMap.forEach((data, dateStr) => { const date = new Date(dateStr); const throughputEntry: ThroughputEntry = { + datasourceId: cycleTimeEntries[0].datasourceId, date: date, throughput: data.count, issueIds: data.issueIds @@ -308,9 +286,9 @@ export class BusinessLogicService { } - async computeWorkInProgress(): Promise { - const issueHistories: IssueHistory[] = await this.storageService.getAllIssueHistories(); - const statuses: Status[] = await this.storageService.getAllStatuses(); + async computeWorkInProgress(datasourceId: number): Promise { + const issueHistories: IssueHistory[] = await this.storageService.getAllIssueHistories(datasourceId); + const statuses: Status[] = await this.storageService.getAllStatuses(datasourceId); const inProgressStatusIds = statuses .filter(status => status.category === StatusCategory.InProgress) @@ -331,6 +309,7 @@ export class BusinessLogicService { const workInProgressEntries: WorkInProgressEntry[] = []; workInProgressMap.forEach((issueIds, dateStr) => { workInProgressEntries.push({ + datasourceId: datasourceId, date: new Date(dateStr), wip: issueIds.size, issueIds: Array.from(issueIds) diff --git a/src/app/services/jira-cloud.service.ts b/src/app/services/jira-cloud.service.ts index 0797d54..4a6ff41 100644 --- a/src/app/services/jira-cloud.service.ts +++ b/src/app/services/jira-cloud.service.ts @@ -105,7 +105,7 @@ export class JiraCloudService implements OnInit { }); //if response successfully received, clear all data - await this.storageService.clearAllData(); + await this.storageService.clearAllIssueData(dataSourceId); // Manually map the response to Issue objects const issues: Issue[] = []; @@ -113,7 +113,7 @@ export class JiraCloudService implements OnInit { let i: Issue = { issueKey: issue.key, title: issue.fields.summary, - dataSourceId: datasource.id!, + datasourceId: datasource.id!, createdDate: new Date(issue.fields.created), status: issue.fields.status!.name!, externalStatusId: Number.parseInt(issue.fields.status!.id!), @@ -132,19 +132,19 @@ export class JiraCloudService implements OnInit { if (entryHolder.canEntries.length > 0) await this.storageService.addCanceledCycleEntries(entryHolder.canEntries); } - const allHistories = await this.storageService.getAllIssueHistories(); + const allHistories = await this.storageService.getAllIssueHistories(dataSourceId); let newStatesFound = this.businessLogicService.findAllNewStatuses(issues, allHistories); - const allStatuses = await this.storageService.getAllStatuses(); + const allStatuses = await this.storageService.getAllStatuses(dataSourceId); newStatesFound = this.businessLogicService.filterOutMappedStatuses(newStatesFound, allStatuses); await this.storageService.addStatuses(newStatesFound); - const cycleTimeEntries = await this.storageService.getCycleTimeData(); + const cycleTimeEntries = await this.storageService.getCycleTimeData(dataSourceId); const throughputs = this.businessLogicService.findThroughputEntries(cycleTimeEntries); await this.storageService.saveThroughputData(throughputs); - const workInProgressEntries = this.businessLogicService.computeWorkInProgress(); + const workInProgressEntries = this.businessLogicService.computeWorkInProgress(dataSourceId); return issues; diff --git a/src/app/services/jira-data-center.service.ts b/src/app/services/jira-data-center.service.ts index 42b2c8f..9150492 100644 --- a/src/app/services/jira-data-center.service.ts +++ b/src/app/services/jira-data-center.service.ts @@ -88,8 +88,8 @@ export class JiraDataCenterService implements OnInit { this.router.navigate([DASHBOARD]); } - async getIssues(dataSetId: number): Promise { - const config = await this.databaseService.getDatasource(dataSetId); + async getIssues(datasourceId: number): Promise { + const config = await this.databaseService.getDatasource(datasourceId); if (config !== null && config?.access_token ) { @@ -111,7 +111,7 @@ export class JiraDataCenterService implements OnInit { const issues: Issue[] = response!.issues!.map((issue: Version3.Version3Models.Issue) => ({ issueKey: issue.key, title: issue.fields.summary, - dataSourceId: config.id!, + datasourceId: config.id!, createdDate: new Date(issue.fields.created), status: issue.fields.status!.name!, externalStatusId: Number.parseInt(issue.fields.status!.id!), diff --git a/src/app/services/storage.service.ts b/src/app/services/storage.service.ts index 4ce3dea..7c56c40 100644 --- a/src/app/services/storage.service.ts +++ b/src/app/services/storage.service.ts @@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'; import {Datasource} from '../models/datasource'; import {Issue} from "../models/issue"; import {WorkItemAgeEntry} from "../models/workItemAgeEntry"; -import {NgxIndexedDBModule, DBConfig, NgxIndexedDBService} from 'ngx-indexed-db'; +import {DBConfig, NgxIndexedDBService} from 'ngx-indexed-db'; import {firstValueFrom} from "rxjs"; import {AppSettings} from "../models/appSettings"; import {IssueHistory} from "../models/issueHistory"; @@ -32,8 +32,7 @@ export const dbConfigCore: DBConfig = { objectStoresMeta: [{ store: TableNames.DATASOURCES, storeConfig: {keyPath: 'id', autoIncrement: true}, - storeSchema: [ - ] + storeSchema: [] }, { store: TableNames.APP_SETTINGS, storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [] @@ -42,55 +41,63 @@ export const dbConfigCore: DBConfig = { storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ {name: 'name', keypath: 'name', options: {unique: false}}, {name: 'category', keypath: 'category', options: {unique: false}}, - {name: 'dataSetId', keypath: 'dataSetId', options: { unique: false}}, + {name: 'datasourceId', keypath: 'datasourceId', options: {unique: false}}, ] }, - ]}; + ] +}; export const dbConfigIssueData: DBConfig = { name: 'metriqs-database-issue-data', version: 1, migrationFactory: migrationFactoryIssues, - objectStoresMeta: [ { + objectStoresMeta: [{ store: TableNames.ISSUES, storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ - {name: 'dataSourceId', keypath: 'dataSourceId', options: {unique: false}}, + {name: 'datasourceId', keypath: 'datasourceId', options: {unique: false}}, {name: 'issueKey', keypath: 'issueKey', options: {unique: false}}, {name: 'id', keypath: 'id', options: {unique: false}}, ] }, { - store: TableNames.WORK_ITEM_AGE, - storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ - {name: 'issueId', keypath: 'issueId', options: {unique: false}}, - {name: 'status', keypath: 'status', options: {unique: false}}, - ] - }, { + store: TableNames.WORK_ITEM_AGE, + storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ + {name: 'issueId', keypath: 'issueId', options: {unique: false}}, + {name: 'status', keypath: 'status', options: {unique: false}}, + {name: 'datasourceId', keypath: 'datasourceId', options: {unique: false}}, + ] + }, { store: TableNames.ISSUE_HISTORY, storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ {name: 'issueId', keypath: 'issueId', options: {unique: false}}, {name: 'field', keypath: 'field', options: {unique: false}}, + {name: 'datasourceId', keypath: 'datasourceId', options: {unique: false}}, ] }, { store: TableNames.CYCLE_TIME, storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ {name: 'issueId', keypath: 'issueId', options: {unique: false}}, + {name: 'datasourceId', keypath: 'datasourceId', options: {unique: false}}, ] }, { store: TableNames.CANCELED_CYCLE, storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ {name: 'issueId', keypath: 'issueId', options: {unique: false}}, + {name: 'datasourceId', keypath: 'datasourceId', options: {unique: false}}, ] }, { store: TableNames.THROUGHPUT, storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ {name: 'issueId', keypath: 'issueId', options: {unique: false}}, + {name: 'datasourceId', keypath: 'datasourceId', options: {unique: false}}, ] }, { store: TableNames.WORK_IN_PROGRESS, - storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [] + storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ + {name: 'datasourceId', keypath: 'datasourceId', options: {unique: false}}, + ] }, ] }; @@ -100,24 +107,22 @@ export const dbConfigIssueData: DBConfig = { export function migrationFactoryIssues() { return { 1: (db: any, transaction: { objectStore: (arg0: string) => any; }) => { - const issues = transaction.objectStore(TableNames.ISSUES); - const workItems = transaction.objectStore(TableNames.WORK_ITEM_AGE); - const issueHistory = transaction.objectStore(TableNames.ISSUE_HISTORY); - const cycleTime = transaction.objectStore(TableNames.CYCLE_TIME); - const canceled = transaction.objectStore(TableNames.CANCELED_CYCLE); - const throughput = transaction.objectStore(TableNames.THROUGHPUT); + transaction.objectStore(TableNames.ISSUES); + transaction.objectStore(TableNames.WORK_ITEM_AGE); + transaction.objectStore(TableNames.ISSUE_HISTORY); + transaction.objectStore(TableNames.CYCLE_TIME); + transaction.objectStore(TableNames.CANCELED_CYCLE); + transaction.objectStore(TableNames.THROUGHPUT); }, }; } export function migrationFactoryCore() { - // The animal table was added with version 2 but none of the existing tables or data needed - // to be modified so a migrator for that version is not included. return { 1: (db: any, transaction: { objectStore: (arg0: string) => any; }) => { - const issues = transaction.objectStore(TableNames.DATASOURCES); - const settings = transaction.objectStore(TableNames.APP_SETTINGS); - const status = transaction.objectStore(TableNames.STATUS); + transaction.objectStore(TableNames.DATASOURCES); + transaction.objectStore(TableNames.APP_SETTINGS); + transaction.objectStore(TableNames.STATUS); }, }; } @@ -142,9 +147,9 @@ export class StorageService { }); } - async addDataset(dataset: Datasource): Promise { + async addDatasource(datasource: Datasource): Promise { this.dbService.selectDb(dbConfigCore.name); - return firstValueFrom(this.dbService.add(TableNames.DATASOURCES, dataset)); + return firstValueFrom(this.dbService.add(TableNames.DATASOURCES, datasource)); } async getAllDatasources(): Promise { @@ -157,24 +162,19 @@ export class StorageService { await firstValueFrom(this.dbService.delete(TableNames.DATASOURCES, id)); } - async updateDatasource(dataset: Datasource): Promise { + async updateDatasource(datasource: Datasource): Promise { this.dbService.selectDb(dbConfigCore.name); - return await firstValueFrom(this.dbService.update(TableNames.DATASOURCES, dataset)); + return await firstValueFrom(this.dbService.update(TableNames.DATASOURCES, datasource)); } - async hasWorkItemAgeData(): Promise { + async getAllIssues(datasourceId: number): Promise { this.dbService.selectDb(dbConfigIssueData.name); - return await firstValueFrom(this.dbService.count(TableNames.WORK_ITEM_AGE)) > 0; + return firstValueFrom(this.dbService.getAllByIndex(TableNames.ISSUES, 'datasourceId', IDBKeyRange.only(datasourceId))); } - async getAllIssues(): Promise { + async getWorkItemAgeData(datasourceId: number): Promise { this.dbService.selectDb(dbConfigIssueData.name); - return firstValueFrom(this.dbService.getAll(TableNames.ISSUES)); - } - - async getWorkItemAgeData(): Promise { - this.dbService.selectDb(dbConfigIssueData.name); - return firstValueFrom(this.dbService.getAll(TableNames.WORK_ITEM_AGE)); + return firstValueFrom(this.dbService.getAllByIndex(TableNames.WORK_ITEM_AGE, 'datasourceId', IDBKeyRange.only(datasourceId))); } async addissue(issue: Issue): Promise { @@ -192,9 +192,9 @@ export class StorageService { return firstValueFrom(this.dbService.bulkAdd(TableNames.WORK_ITEM_AGE, workItemAgeEntries)); } - async createDataset(newDataset: Datasource) { + async createDatasource(datasource: Datasource) { this.dbService.selectDb(dbConfigCore.name); - return firstValueFrom(this.dbService.add(TableNames.DATASOURCES, newDataset)); + return firstValueFrom(this.dbService.add(TableNames.DATASOURCES, datasource)); } getDatasource(id: number): Promise { @@ -218,13 +218,25 @@ export class StorageService { return firstValueFrom(this.dbService.deleteDatabase()); } - async clearAllData() { + async clearAllIssueData(datasourceId: number) { this.dbService.selectDb(dbConfigIssueData.name); - await firstValueFrom(this.dbService.clear(TableNames.ISSUE_HISTORY)); - await firstValueFrom(this.dbService.clear(TableNames.ISSUES)); - await firstValueFrom(this.dbService.clear(TableNames.WORK_ITEM_AGE)); - await firstValueFrom(this.dbService.clear(TableNames.CYCLE_TIME)); - await firstValueFrom(this.dbService.clear(TableNames.THROUGHPUT)); + + const tables = [ + TableNames.ISSUES, + TableNames.ISSUE_HISTORY, + TableNames.WORK_ITEM_AGE, + TableNames.CYCLE_TIME, + TableNames.CANCELED_CYCLE, + TableNames.THROUGHPUT, + TableNames.WORK_IN_PROGRESS + ]; + + for (const table of tables) { + const entries = await firstValueFrom(this.dbService.getAllByIndex(table, 'datasourceId', IDBKeyRange.only(datasourceId))); + for (const entry of entries) { + await firstValueFrom(this.dbService.delete(table, (entry as any).id)); + } + } } async addIssueHistories(histories: IssueHistory[]): Promise { @@ -232,9 +244,9 @@ export class StorageService { return firstValueFrom(this.dbService.bulkAdd(TableNames.ISSUE_HISTORY, histories)); } - async getAllIssueHistories(): Promise { + async getAllIssueHistories(datasourceId: number): Promise { this.dbService.selectDb(dbConfigIssueData.name); - return firstValueFrom(this.dbService.getAll(TableNames.ISSUE_HISTORY)); + return firstValueFrom(this.dbService.getAllByIndex(TableNames.ISSUE_HISTORY, 'datasourceId', IDBKeyRange.only(datasourceId))); } async addCycleTimeEntries(cycleTimes: CycleTimeEntry[]) { @@ -242,9 +254,9 @@ export class StorageService { return firstValueFrom(this.dbService.bulkAdd(TableNames.CYCLE_TIME, cycleTimes)); } - async getCycleTimeData() { + async getCycleTimeData(datasourceId: number) { this.dbService.selectDb(dbConfigIssueData.name); - return firstValueFrom(this.dbService.getAll(TableNames.CYCLE_TIME)); + return firstValueFrom(this.dbService.getAllByIndex(TableNames.CYCLE_TIME, 'datasourceId', IDBKeyRange.only(datasourceId))); } async addStatuses(status: Status[]) { @@ -252,9 +264,9 @@ export class StorageService { return firstValueFrom(this.dbService.bulkAdd(TableNames.STATUS, status)); } - async getThroughputData(): Promise { + async getThroughputData(datasourceId: number): Promise { this.dbService.selectDb(dbConfigIssueData.name); - return firstValueFrom(this.dbService.getAll(TableNames.THROUGHPUT)); + return firstValueFrom(this.dbService.getAllByIndex(TableNames.THROUGHPUT, 'datasourceId', IDBKeyRange.only(datasourceId))); } async addThroughputData(throughput: ThroughputEntry[]): Promise { @@ -263,9 +275,9 @@ export class StorageService { } - getAllStatuses() { + getAllStatuses(datasourceId: number) { this.dbService.selectDb(dbConfigCore.name); - return firstValueFrom(this.dbService.getAll(TableNames.STATUS)); + return firstValueFrom(this.dbService.getAllByIndex(TableNames.STATUS, 'datasourceId', IDBKeyRange.only(datasourceId))); } updateStatus(status: Status) { @@ -279,9 +291,10 @@ export class StorageService { return firstValueFrom(this.dbService.getAllByIndex(TableNames.ISSUE_HISTORY, 'issueId', IDBKeyRange.only(issue.id))); } - getAllIssueHistoriesForStatuses() { + async getAllIssueHistoriesForStatuses(datasourceId: number): Promise { this.dbService.selectDb(dbConfigIssueData.name); - return firstValueFrom(this.dbService.getAllByIndex(TableNames.ISSUE_HISTORY, 'field', IDBKeyRange.only("status"))); + const issueHistories = await firstValueFrom(this.dbService.getAllByIndex(TableNames.ISSUE_HISTORY, 'field', IDBKeyRange.only('status'))); + return issueHistories.filter(history => history.datasourceId === datasourceId); } async getIssuesByIds(issuesIds: number[]): Promise { @@ -301,9 +314,9 @@ export class StorageService { return firstValueFrom(this.dbService.bulkAdd(TableNames.THROUGHPUT, througputs)); } - async getWorkInProgressData() { + async getWorkInProgressData(datasourceId: number): Promise { this.dbService.selectDb(dbConfigIssueData.name); - return firstValueFrom(this.dbService.getAll(TableNames.WORK_IN_PROGRESS)); + return firstValueFrom(this.dbService.getAllByIndex(TableNames.WORK_IN_PROGRESS, 'datasourceId', IDBKeyRange.only(datasourceId))); } saveWorkInProgressData(workInProgressEntries: WorkInProgressEntry[]) {