From a68840dabc5648449d1c191757d848c54ca1042a Mon Sep 17 00:00:00 2001 From: mario meltzow Date: Tue, 20 Aug 2024 12:50:52 +0200 Subject: [PATCH 1/4] fixing displaying throughput --- src/app/components/layout/layout.component.ts | 2 +- .../throughput-page.component.ts | 18 ++--------- src/app/models/throughput.ts | 8 ----- src/app/models/throughputEntry.ts | 6 ++++ src/app/models/workInProgressEntry.ts | 6 ++++ src/app/services/business-logic.service.ts | 32 +++++++++++++++++++ src/app/services/jira-cloud.service.ts | 4 +++ src/app/services/storage.service.ts | 31 ++++++++++++++---- 8 files changed, 75 insertions(+), 32 deletions(-) delete mode 100644 src/app/models/throughput.ts create mode 100644 src/app/models/throughputEntry.ts create mode 100644 src/app/models/workInProgressEntry.ts diff --git a/src/app/components/layout/layout.component.ts b/src/app/components/layout/layout.component.ts index c8b73d2..1022a28 100644 --- a/src/app/components/layout/layout.component.ts +++ b/src/app/components/layout/layout.component.ts @@ -139,7 +139,7 @@ export class LayoutComponent implements OnInit { } async clearDatabase() { - const success = await this.storageService.clearIssueData(); + const success = await this.storageService.recreateDatabase(); if (success) { this.toastr.success('Successfully cleared all data'); } else { diff --git a/src/app/components/throughput-page/throughput-page.component.ts b/src/app/components/throughput-page/throughput-page.component.ts index 6ceb8cc..4385c9f 100644 --- a/src/app/components/throughput-page/throughput-page.component.ts +++ b/src/app/components/throughput-page/throughput-page.component.ts @@ -4,7 +4,7 @@ import { CommonModule } from '@angular/common'; import { BaseChartDirective } from 'ng2-charts'; import { ChartConfiguration, ChartType } from 'chart.js'; import { StorageService } from '../../services/storage.service'; -import { Throughput } from '../../models/throughput'; +import {ThroughputEntry} from '../../models/throughputEntry'; @Component({ selector: 'app-throughput-page', @@ -41,22 +41,8 @@ export class ThroughputPageComponent implements OnInit { constructor(private storageService: StorageService) {} - //FIXME - async saveExampleThroughputData(): Promise { - const exampleData: Throughput[] = [ - { throughput: 5, date: new Date('2023-01-01'), issueId: 1, issueKey: 'KEY-1', title: 'Title 1' }, - { throughput: 10, date: new Date('2023-01-02'), issueId: 2, issueKey: 'KEY-2', title: 'Title 2' }, - { throughput: 15, date: new Date('2023-01-03'), issueId: 3, issueKey: 'KEY-3', title: 'Title 3' }, - { throughput: 20, date: new Date('2023-01-04'), issueId: 4, issueKey: 'KEY-4', title: 'Title 4' }, - { throughput: 25, date: new Date('2023-01-05'), issueId: 5, issueKey: 'KEY-5', title: 'Title 5' }, - ]; - await this.storageService.addThroughputData(exampleData); - } - async ngOnInit() { - await this.saveExampleThroughputData(); - - const throughputData: Throughput[] = await this.storageService.getThroughputData(); + const throughputData: ThroughputEntry[] = await this.storageService.getThroughputData(); this.lineChartData.datasets[0].data = throughputData.map(entry => entry.throughput); this.lineChartData.labels = throughputData.map(entry => entry.date.toDateString()); this.chart?.update(); diff --git a/src/app/models/throughput.ts b/src/app/models/throughput.ts deleted file mode 100644 index e08a63e..0000000 --- a/src/app/models/throughput.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Throughput { - issueId: number; - issueKey: string; - title: string; - throughput: number; - date: Date; - id?: number; -} diff --git a/src/app/models/throughputEntry.ts b/src/app/models/throughputEntry.ts new file mode 100644 index 0000000..244bf2f --- /dev/null +++ b/src/app/models/throughputEntry.ts @@ -0,0 +1,6 @@ +export interface ThroughputEntry { + issueIds: number[]; + throughput: number; + date: Date; + id?: number; +} diff --git a/src/app/models/workInProgressEntry.ts b/src/app/models/workInProgressEntry.ts new file mode 100644 index 0000000..8bb79f5 --- /dev/null +++ b/src/app/models/workInProgressEntry.ts @@ -0,0 +1,6 @@ +export interface ThroughputEntry { + issueIds: number[]; + wip: number; + date: Date; + id?: number; +} diff --git a/src/app/services/business-logic.service.ts b/src/app/services/business-logic.service.ts index 1b431cb..c262b24 100644 --- a/src/app/services/business-logic.service.ts +++ b/src/app/services/business-logic.service.ts @@ -6,6 +6,8 @@ import {IssueHistory} from "../models/issueHistory"; import {CycleTimeEntry} from "../models/cycleTimeEntry"; import {Status, StatusCategory} from "../models/status"; import {CanceledCycleEntry} from "../models/canceledCycleEntry"; +import {ThroughputEntry} from "../models/throughputEntry"; +import {count} from "rxjs"; @Injectable({ providedIn: 'root' @@ -269,4 +271,34 @@ export class BusinessLogicService { return {wiaEntry: workItemAgeEntry!, canEntries: canceledCycleEntries, cycleTEntries: cycleTimeEntries}; } + + findThroughputEntries(cycleTimeEntries: CycleTimeEntry[]): ThroughputEntry[] { + const throughputEntries: ThroughputEntry[] = []; + const throughputMap: Map = new Map(); + + // Count the number of CycleTimeEntries resolved on the same day and collect issue IDs + cycleTimeEntries.forEach(entry => { + const resolvedDateStr = entry.resolvedDate.toISOString().split('T')[0]; // Get the date part only + if (throughputMap.has(resolvedDateStr)) { + const entryData = throughputMap.get(resolvedDateStr)!; + entryData.count += 1; + entryData.issueIds.push(entry.issueId); + } else { + throughputMap.set(resolvedDateStr, {count: 1, issueIds: [entry.issueId]}); + } + }); + + // Create ThroughputEntry objects using the counts from the map + throughputMap.forEach((data, dateStr) => { + const date = new Date(dateStr); + const throughputEntry: ThroughputEntry = { + date: date, + throughput: data.count, + issueIds: data.issueIds + }; + throughputEntries.push(throughputEntry); + }); + + return throughputEntries; + } } diff --git a/src/app/services/jira-cloud.service.ts b/src/app/services/jira-cloud.service.ts index ed05615..6475f6e 100644 --- a/src/app/services/jira-cloud.service.ts +++ b/src/app/services/jira-cloud.service.ts @@ -140,6 +140,10 @@ export class JiraCloudService implements OnInit { newStatesFound = this.businessLogicService.filterOutMappedStatuses(newStatesFound, allStatuses); await this.storageService.addStatuses(newStatesFound); + const cycleTimeEntries = await this.storageService.getCycleTimeData(); + const throughputs = this.businessLogicService.findThroughputEntries(cycleTimeEntries); + await this.storageService.saveThroughputData(throughputs); + return issues; } catch (error) { this.toastr.error('Failed to fetch issues from Jira', error!.toString()); diff --git a/src/app/services/storage.service.ts b/src/app/services/storage.service.ts index 851a0b4..1eb042b 100644 --- a/src/app/services/storage.service.ts +++ b/src/app/services/storage.service.ts @@ -8,7 +8,7 @@ import {AppSettings} from "../models/appSettings"; import {IssueHistory} from "../models/issueHistory"; import {CycleTimeEntry} from "../models/cycleTimeEntry"; import {Status} from "../models/status"; -import {Throughput} from "../models/throughput"; +import {ThroughputEntry} from "../models/throughputEntry"; import {CanceledCycleEntry} from "../models/canceledCycleEntry"; export class TableNames { @@ -94,6 +94,7 @@ export const dbConfigIssueData: DBConfig = { ] }; + // Ahead of time compiles requires an exported function for factories export function migrationFactory() { return { @@ -128,6 +129,18 @@ export class StorageService { constructor(private dbService: NgxIndexedDBService) { } + async recreateDatabase(): Promise { + return this.deleteIssueDatabase().then(async () => { + for (const storeMeta of dbConfigIssueData.objectStoresMeta) { + const store = await this.dbService.createObjectStore(storeMeta, migrationFactory); + // storeMeta.storeSchema.forEach(schema => { + // store.createIndex(schema.name, schema.keypath, schema.options); + // }); + } + return true; + }); + } + async addDataset(dataset: Dataset): Promise { this.dbService.selectDb(dataSetDbConfig.name); return firstValueFrom(this.dbService.add(TableNames.DATASETS, dataset)); @@ -199,8 +212,7 @@ export class StorageService { return firstValueFrom(this.dbService.getAll(TableNames.APP_SETTINGS)).then(datasets => datasets[0]); } - //FIXME: this must be dependent on the dataset - async clearIssueData() { + async deleteIssueDatabase() { this.dbService.selectDb(dbConfigIssueData.name); return firstValueFrom(this.dbService.deleteDatabase()); } @@ -239,14 +251,14 @@ export class StorageService { return firstValueFrom(this.dbService.bulkAdd(TableNames.STATUS, status)); } - async getThroughputData(): Promise { + async getThroughputData(): Promise { this.dbService.selectDb(dbConfigIssueData.name); - return firstValueFrom(this.dbService.getAll(TableNames.THROUGHPUT)); + return firstValueFrom(this.dbService.getAll(TableNames.THROUGHPUT)); } - async addThroughputData(throughput: Throughput[]): Promise { + async addThroughputData(throughput: ThroughputEntry[]): Promise { this.dbService.selectDb(dbConfigIssueData.name); - return firstValueFrom(this.dbService.bulkAdd(TableNames.THROUGHPUT, throughput)); + return firstValueFrom(this.dbService.bulkAdd(TableNames.THROUGHPUT, throughput)); } @@ -282,4 +294,9 @@ export class StorageService { return firstValueFrom(this.dbService.bulkAdd(TableNames.CANCELED_CYCLE, canEntries)); } + + async saveThroughputData(througputs: ThroughputEntry[]) { + this.dbService.selectDb(dbConfigIssueData.name); + return firstValueFrom(this.dbService.bulkAdd(TableNames.THROUGHPUT, througputs)); + } } From 44d279417503255b10ed2b65586a4f2edb33a4b3 Mon Sep 17 00:00:00 2001 From: mario meltzow Date: Tue, 20 Aug 2024 13:55:04 +0200 Subject: [PATCH 2/4] fixing displaying throughput --- .../throughput-page.component.ts | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/app/components/throughput-page/throughput-page.component.ts b/src/app/components/throughput-page/throughput-page.component.ts index 4385c9f..995d34a 100644 --- a/src/app/components/throughput-page/throughput-page.component.ts +++ b/src/app/components/throughput-page/throughput-page.component.ts @@ -43,8 +43,37 @@ export class ThroughputPageComponent implements OnInit { async ngOnInit() { const throughputData: ThroughputEntry[] = await this.storageService.getThroughputData(); - this.lineChartData.datasets[0].data = throughputData.map(entry => entry.throughput); - this.lineChartData.labels = throughputData.map(entry => entry.date.toDateString()); + const allDates = this.generateAllDates(throughputData); + const mappedData = this.mapDataToDates(allDates, throughputData); + + this.lineChartData.datasets[0].data = mappedData.map(entry => entry.throughput); + this.lineChartData.labels = mappedData.map(entry => entry.date.toDateString()); this.chart?.update(); } + + private generateAllDates(data: ThroughputEntry[]): { date: Date, throughput: number }[] { + if (data.length === 0) return []; + + const startDate = new Date(Math.min(...data.map(entry => entry.date.getTime()))); + const endDate = new Date(Math.max(...data.map(entry => entry.date.getTime()))); + const allDates = []; + + for (let date = new Date(startDate); date <= endDate; date.setDate(date.getDate() + 1)) { + allDates.push({date: new Date(date), throughput: 0}); + } + + return allDates; + } + + private mapDataToDates(allDates: { date: Date, throughput: number }[], data: ThroughputEntry[]): { + date: Date, + throughput: number + }[] { + const dataMap = new Map(data.map(entry => [entry.date.toDateString(), entry.throughput])); + + return allDates.map(entry => ({ + date: entry.date, + throughput: dataMap.get(entry.date.toDateString()) || 0 + })); + } } From 0c332dc655c409b8a5dd200e715933a9c997d144 Mon Sep 17 00:00:00 2001 From: mario meltzow Date: Tue, 20 Aug 2024 14:20:56 +0200 Subject: [PATCH 3/4] refactoring dataset => datasource --- src/app/app-routing.module.ts | 16 ++-- src/app/app.module.ts | 8 +- .../datasource-edit.component.html} | 19 ++--- .../datasource-edit.component.scss} | 0 .../datasource-edit.component.spec.ts} | 10 +-- .../datasource-edit.component.ts} | 56 +++++++------- .../datasource/datasource-list.component.html | 17 +++++ .../datasource-list.component.scss} | 0 .../datasource-list.component.ts} | 33 ++++----- .../components/layout/layout.component.html | 10 +-- src/app/components/layout/layout.component.ts | 52 ++++++------- .../manage-datasets.component.html | 17 ----- src/app/models/appSettings.ts | 2 +- src/app/models/{dataset.ts => datasource.ts} | 6 +- src/app/models/issue.ts | 2 +- src/app/models/issueHistory.ts | 2 +- src/app/models/status.ts | 2 +- .../services/business-logic.service.spec.ts | 42 +++++------ src/app/services/business-logic.service.ts | 14 ++-- src/app/services/jira-cloud.service.ts | 26 +++---- src/app/services/jira-data-center.service.ts | 12 +-- src/app/services/storage.service.ts | 73 +++++++++---------- 22 files changed, 209 insertions(+), 210 deletions(-) rename src/app/components/{edit-dataset/edit-dataset.component.html => datasource/datasource-edit.component.html} (56%) rename src/app/components/{edit-dataset/edit-dataset.component.scss => datasource/datasource-edit.component.scss} (100%) rename src/app/components/{edit-dataset/edit-dataset.component.spec.ts => datasource/datasource-edit.component.spec.ts} (57%) rename src/app/components/{edit-dataset/edit-dataset.component.ts => datasource/datasource-edit.component.ts} (56%) create mode 100644 src/app/components/datasource/datasource-list.component.html rename src/app/components/{manage-datasets/manage-datasets.component.scss => datasource/datasource-list.component.scss} (100%) rename src/app/components/{manage-datasets/manage-datasets.component.ts => datasource/datasource-list.component.ts} (57%) delete mode 100644 src/app/components/manage-datasets/manage-datasets.component.html rename src/app/models/{dataset.ts => datasource.ts} (72%) diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 022cb8c..7dbc25e 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,10 +3,10 @@ import {RouterModule, Routes} from '@angular/router'; import { DashboardComponent } from './components/dashboard/dashboard.component'; import {WorkItemAgePage} from './components/work-item-age-page/work-item-age.page'; import {NgModule} from "@angular/core"; -import {ManageDatasetsComponent} from "./components/manage-datasets/manage-datasets.component"; +import {DatasourceListComponent} from "./components/datasource/datasource-list.component"; import {CallbackComponent} from "./components/callback.component"; import {CycleTimePage} from "./components/cycle-time-page/cycle-time.page"; -import {EditDatasetComponent} from "./components/edit-dataset/edit-dataset.component"; +import {DatasourceEditComponent} from "./components/datasource/datasource-edit.component"; import {ThroughputPageComponent} from "./components/throughput-page/throughput-page.component"; export const CALLBACK_JIRA_CLOUD = 'callbackJiraCloud'; @@ -14,9 +14,9 @@ export const CALLBACK_JIRA_DATA_CENTER = 'callbackJiraDataCenter'; export const DASHBOARD = 'dashboard'; export const WORK_ITEM_AGE = 'work-item-age'; export const CYCLE_TIME = 'cycle-time'; -export const MANAGE_DATASETS = 'datasets'; -export const CREATE_DATASETS = MANAGE_DATASETS + '/create'; -export const THROUGHPUT = 'troughput'; +export const DATASOURCE_LIST = 'datasources'; +export const DATASOURCE_CREATE = DATASOURCE_LIST + '/create'; +export const THROUGHPUT = 'throughput'; export const routes: Routes = [ {path: '', redirectTo: `/${DASHBOARD}`, pathMatch: 'full'}, @@ -26,9 +26,9 @@ export const routes: Routes = [ {path: WORK_ITEM_AGE, component: WorkItemAgePage}, {path: THROUGHPUT, component: ThroughputPageComponent}, {path: CYCLE_TIME, component: CycleTimePage}, - {path: MANAGE_DATASETS, component: ManageDatasetsComponent}, - {path: MANAGE_DATASETS + '/:id', component: EditDatasetComponent}, - {path: CREATE_DATASETS, component: EditDatasetComponent}, + {path: DATASOURCE_LIST, component: DatasourceListComponent}, + {path: DATASOURCE_LIST + '/:id', component: DatasourceEditComponent}, + {path: DATASOURCE_CREATE, component: DatasourceEditComponent}, ]; @NgModule({ diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5329661..2304ea5 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; -import {StorageService, dbConfigIssueData, dataSetDbConfig} from './services/storage.service'; +import {StorageService, dbConfigIssueData, dbConfigCore} from './services/storage.service'; import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; import { ToastrModule } from 'ngx-toastr'; import { NgxIndexedDBModule } from 'ngx-indexed-db'; @@ -9,7 +9,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import {AppRoutingModule, routes} from "./app-routing.module"; import {AppComponent} from "./app.component"; import {RouterModule} from "@angular/router"; -import {ManageDatasetsComponent} from "./components/manage-datasets/manage-datasets.component"; +import {DatasourceListComponent} from "./components/datasource/datasource-list.component"; import {DragDropModule} from "@angular/cdk/drag-drop"; import {MatChipsModule} from "@angular/material/chips"; @@ -19,12 +19,12 @@ import {MatChipsModule} from "@angular/material/chips"; imports: [ BrowserModule, AppRoutingModule, - NgxIndexedDBModule.forRoot(dbConfigIssueData, dataSetDbConfig), + NgxIndexedDBModule.forRoot(dbConfigIssueData, dbConfigCore), FormsModule, ToastrModule.forRoot(), BrowserAnimationsModule, RouterModule.forRoot(routes), - ManageDatasetsComponent, + DatasourceListComponent, DragDropModule, MatChipsModule ], diff --git a/src/app/components/edit-dataset/edit-dataset.component.html b/src/app/components/datasource/datasource-edit.component.html similarity index 56% rename from src/app/components/edit-dataset/edit-dataset.component.html rename to src/app/components/datasource/datasource-edit.component.html index e5c13f2..82a8c58 100644 --- a/src/app/components/edit-dataset/edit-dataset.component.html +++ b/src/app/components/datasource/datasource-edit.component.html @@ -1,39 +1,40 @@ -
+ Name - Name is required + Name is required Base URL - Base URL is required - Invalid URL format + Base URL is required + Invalid URL format JQL - JQL is required + JQL is required Type - {{ type }} + {{ type }} - Type is required + Type is required - - + +
diff --git a/src/app/components/edit-dataset/edit-dataset.component.scss b/src/app/components/datasource/datasource-edit.component.scss similarity index 100% rename from src/app/components/edit-dataset/edit-dataset.component.scss rename to src/app/components/datasource/datasource-edit.component.scss diff --git a/src/app/components/edit-dataset/edit-dataset.component.spec.ts b/src/app/components/datasource/datasource-edit.component.spec.ts similarity index 57% rename from src/app/components/edit-dataset/edit-dataset.component.spec.ts rename to src/app/components/datasource/datasource-edit.component.spec.ts index efd667b..a952f6d 100644 --- a/src/app/components/edit-dataset/edit-dataset.component.spec.ts +++ b/src/app/components/datasource/datasource-edit.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { EditDatasetComponent } from './edit-dataset.component'; +import {DatasourceEditComponent} from './datasource-edit.component'; describe('EditDatasetComponent', () => { - let component: EditDatasetComponent; - let fixture: ComponentFixture; + let component: DatasourceEditComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [EditDatasetComponent] + imports: [DatasourceEditComponent] }) .compileComponents(); - fixture = TestBed.createComponent(EditDatasetComponent); + fixture = TestBed.createComponent(DatasourceEditComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/components/edit-dataset/edit-dataset.component.ts b/src/app/components/datasource/datasource-edit.component.ts similarity index 56% rename from src/app/components/edit-dataset/edit-dataset.component.ts rename to src/app/components/datasource/datasource-edit.component.ts index dd49899..3e81333 100644 --- a/src/app/components/edit-dataset/edit-dataset.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 {Dataset, DataSetType} from '../../models/dataset'; +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"; @@ -10,13 +10,13 @@ import {MatButton} from "@angular/material/button"; import {MatOption, MatSelect} from "@angular/material/select"; import {NgForOf, NgIf} from "@angular/common"; import {ToastrService} from "ngx-toastr"; -import {MANAGE_DATASETS} from "../../app-routing.module"; +import {DATASOURCE_LIST} from "../../app-routing.module"; import {Status} from "../../models/status"; import {StatusMappingComponent} from "../status-mapping/status-mapping.component"; @Component({ selector: 'app-edit-dataset', - templateUrl: './edit-dataset.component.html', + templateUrl: './datasource-edit.component.html', standalone: true, imports: [ ReactiveFormsModule, @@ -31,12 +31,12 @@ import {StatusMappingComponent} from "../status-mapping/status-mapping.component NgForOf, NgIf, MatError, StatusMappingComponent ], - styleUrls: ['./edit-dataset.component.scss'] + styleUrls: ['./datasource-edit.component.scss'] }) -export class EditDatasetComponent implements OnInit { - datasetForm: FormGroup; - datasetId?: number; - datasetTypes = Object.values(DataSetType); +export class DatasourceEditComponent implements OnInit { + datasourceForm: FormGroup; + datasourceId?: number; + datasourceTypes = Object.values(DataSourceType); currentStates: Status[] = []; constructor( @@ -46,50 +46,50 @@ export class EditDatasetComponent implements OnInit { private storageService: StorageService, private toastr: ToastrService ) { - this.datasetForm = this.fb.group({ + this.datasourceForm = this.fb.group({ name: [''], baseUrl: ['', Validators.required], jql: ['', Validators.required], - type: [this.datasetTypes[0]] + type: [this.datasourceTypes[0]] }); } async ngOnInit() { - this.datasetId = +this.route.snapshot.paramMap.get('id')!; - if (this.datasetId !== undefined && this.datasetId > 0) { - const dataset = await this.storageService.getDataset(this.datasetId); - this.datasetForm.patchValue({ + this.datasourceId = +this.route.snapshot.paramMap.get('id')!; + if (this.datasourceId !== undefined && this.datasourceId > 0) { + const dataset = await this.storageService.getDatasource(this.datasourceId); + this.datasourceForm.patchValue({ name: dataset.name ?? '', baseUrl: dataset.baseUrl ?? '', jql: dataset.jql ?? '', - type: dataset.type ?? this.datasetTypes[0] + type: dataset.type ?? this.datasourceTypes[0] }); this.currentStates = await this.storageService.getAllStatuses(); } } async saveDataset(): Promise { - if (this.datasetForm.valid) { - const updatedDataset: Dataset = { - ...this.datasetForm.value + if (this.datasourceForm.valid) { + const updatedDataset: Datasource = { + ...this.datasourceForm.value }; - if (this.datasetId !== undefined && !isNaN(this.datasetId)) { - updatedDataset.id = this.datasetId; - await this.storageService.updateDataset(updatedDataset); - this.toastr.success('Dataset updated'); + if (this.datasourceId !== undefined && !isNaN(this.datasourceId)) { + updatedDataset.id = this.datasourceId; + await this.storageService.updateDatasource(updatedDataset); + this.toastr.success('Datasource updated'); } else { await this.storageService.createDataset(updatedDataset); - this.toastr.success('Dataset created'); + this.toastr.success('Datasource created'); } - this.router.navigate([MANAGE_DATASETS]); + this.router.navigate([DATASOURCE_LIST]); } } async deleteDataset(): Promise { - if (this.datasetId) { - await this.storageService.removeDataset(this.datasetId); - this.toastr.success('Dataset deleted'); - this.router.navigate([MANAGE_DATASETS]); + if (this.datasourceId) { + await this.storageService.removeDatasource(this.datasourceId); + this.toastr.success('Datasource deleted'); + this.router.navigate([DATASOURCE_LIST]); } } } diff --git a/src/app/components/datasource/datasource-list.component.html b/src/app/components/datasource/datasource-list.component.html new file mode 100644 index 0000000..dbcab0b --- /dev/null +++ b/src/app/components/datasource/datasource-list.component.html @@ -0,0 +1,17 @@ +
+

Manage Datasource

+ + + +
+ {{ datasource.name }} - {{ datasource.jql }} + + +
+
+
+
diff --git a/src/app/components/manage-datasets/manage-datasets.component.scss b/src/app/components/datasource/datasource-list.component.scss similarity index 100% rename from src/app/components/manage-datasets/manage-datasets.component.scss rename to src/app/components/datasource/datasource-list.component.scss diff --git a/src/app/components/manage-datasets/manage-datasets.component.ts b/src/app/components/datasource/datasource-list.component.ts similarity index 57% rename from src/app/components/manage-datasets/manage-datasets.component.ts rename to src/app/components/datasource/datasource-list.component.ts index 6b5271a..9055877 100644 --- a/src/app/components/manage-datasets/manage-datasets.component.ts +++ b/src/app/components/datasource/datasource-list.component.ts @@ -1,4 +1,3 @@ -// src/app/components/manage-datasets/manage-datasets.component.ts import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MatFormField, MatLabel } from "@angular/material/form-field"; @@ -10,14 +9,14 @@ import { NgForOf, NgIf } from "@angular/common"; import { MatInput } from "@angular/material/input"; import { MatSelect, MatOption } from "@angular/material/select"; import { StorageService } from '../../services/storage.service'; -import {Dataset} from '../../models/dataset'; +import {Datasource} from '../../models/datasource'; import {LayoutComponent} from "../layout/layout.component"; -import {CREATE_DATASETS, MANAGE_DATASETS} from "../../app-routing.module"; +import {DATASOURCE_CREATE, DATASOURCE_LIST} from "../../app-routing.module"; import {Router} from "@angular/router"; @Component({ - selector: 'app-manage-datasets', - templateUrl: './manage-datasets.component.html', + selector: 'app-datasources-list', + templateUrl: './datasource-list.component.html', standalone: true, imports: [ MatFormField, @@ -35,33 +34,31 @@ import {Router} from "@angular/router"; MatSelect, MatOption ], - styleUrls: ['./manage-datasets.component.scss'] + styleUrls: ['./datasource-list.component.scss'] }) -export class ManageDatasetsComponent implements OnInit { - datasets: Dataset[] = []; +export class DatasourceListComponent implements OnInit { + datasources: Datasource[] = []; constructor( private storageService: StorageService, - private fb: FormBuilder, - private layoutComponent: LayoutComponent, private router: Router ) { } async ngOnInit() { - this.datasets = await this.storageService.getAllDatasets(); + this.datasources = await this.storageService.getAllDatasources(); } - async removeDataset(dataset: Dataset) { - await this.storageService.removeDataset(dataset.id!); - this.datasets = await this.storageService.getAllDatasets(); + async removeDatasource(datasource: Datasource) { + await this.storageService.removeDatasource(datasource.id!); + this.datasources = await this.storageService.getAllDatasources(); } - editDataset(dataset: Dataset) { - this.router.navigate([MANAGE_DATASETS, dataset.id]); + editDatasource(datasource: Datasource) { + this.router.navigate([DATASOURCE_LIST, datasource.id]); } - createDataset() { - this.router.navigate([CREATE_DATASETS]); + createDatasource() { + this.router.navigate([DATASOURCE_CREATE]); } } diff --git a/src/app/components/layout/layout.component.html b/src/app/components/layout/layout.component.html index ea47968..5607938 100644 --- a/src/app/components/layout/layout.component.html +++ b/src/app/components/layout/layout.component.html @@ -29,14 +29,14 @@ clear Database - Select Dataset - - {{ dataset.name }} + Select Datasource + + {{ datasource.name }} - - - -
- {{ dataset.name }} - {{ dataset.jql }} - - -
-
-
- diff --git a/src/app/models/appSettings.ts b/src/app/models/appSettings.ts index 77ed74d..2996f33 100644 --- a/src/app/models/appSettings.ts +++ b/src/app/models/appSettings.ts @@ -1,3 +1,3 @@ export interface AppSettings { - selectedDatasetId: number; + selectedDatasourceId: number; } diff --git a/src/app/models/dataset.ts b/src/app/models/datasource.ts similarity index 72% rename from src/app/models/dataset.ts rename to src/app/models/datasource.ts index 662ef08..c0ad400 100644 --- a/src/app/models/dataset.ts +++ b/src/app/models/datasource.ts @@ -1,11 +1,11 @@ -export enum DataSetType { +export enum DataSourceType { JIRA_CLOUD = 'JIRA_CLOUD', JIRA_DATACENTER = 'JIRA_DATACENTER', } //FIXME: rename this to datasource -export interface Dataset { - type: DataSetType; +export interface Datasource { + type: DataSourceType; baseUrl: string; access_token: string; cloudId?: string; diff --git a/src/app/models/issue.ts b/src/app/models/issue.ts index 565bfc9..9f17d71 100644 --- a/src/app/models/issue.ts +++ b/src/app/models/issue.ts @@ -1,5 +1,5 @@ export interface Issue { - dataSetId: number; + dataSourceId: number; issueKey: string; title: string; createdDate: Date; diff --git a/src/app/models/issueHistory.ts b/src/app/models/issueHistory.ts index 2e5ea4c..33c84ac 100644 --- a/src/app/models/issueHistory.ts +++ b/src/app/models/issueHistory.ts @@ -1,5 +1,5 @@ export interface IssueHistory { - datasetId: number; + datasourceId: number; id?: number; issueId: number; fromValue: string; diff --git a/src/app/models/status.ts b/src/app/models/status.ts index d305ca0..6a170f1 100644 --- a/src/app/models/status.ts +++ b/src/app/models/status.ts @@ -5,7 +5,7 @@ export enum StatusCategory { } export interface Status { - dataSetId: number; + dataSourceId: number; id?: number; name: string; color?: string; diff --git a/src/app/services/business-logic.service.spec.ts b/src/app/services/business-logic.service.spec.ts index af9ef99..2fca465 100644 --- a/src/app/services/business-logic.service.spec.ts +++ b/src/app/services/business-logic.service.spec.ts @@ -34,12 +34,12 @@ describe('BusinessLogicService', () => { const statuses: Status[] = [ { externalId: 1, category: StatusCategory.Done, - dataSetId: 0, + dataSourceId: 0, name: 'resolved' }, { externalId: 2, category: StatusCategory.Done, - dataSetId: 0, + dataSourceId: 0, name: 'wont do' } ]; @@ -59,12 +59,12 @@ describe('BusinessLogicService', () => { const statuses: Status[] = [ { externalId: 1, category: StatusCategory.InProgress, - dataSetId: 0, + dataSourceId: 0, name: 'In Arbeit' }, { externalId: 2, category: StatusCategory.InProgress, - dataSetId: 0, + dataSourceId: 0, name: 'in review' } ]; @@ -88,19 +88,19 @@ describe('BusinessLogicService', () => { const statuses: Status[] = [ { externalId: 2, category: StatusCategory.InProgress, - dataSetId: 0, + dataSourceId: 0, name: 'in review', order: 2 // the second in-progress status }, { externalId: 1, category: StatusCategory.InProgress, - dataSetId: 0, + dataSourceId: 0, name: 'In Arbeit', order: 1 // the first in-progress status }, { externalId: 3, category: StatusCategory.Done, - dataSetId: 0, + dataSourceId: 0, name: 'Done', order: 3 } @@ -117,7 +117,7 @@ describe('BusinessLogicService', () => { id: 1, issueKey: 'ISSUE-1', title: 'Test Issue', - dataSetId: 1, + dataSourceId: 1, createdDate: new Date('2023-01-01'), status: 'Done', externalStatusId: 3, @@ -127,7 +127,7 @@ describe('BusinessLogicService', () => { const issueHistories: IssueHistory[] = [ { issueId: 1, - datasetId: 1, + datasourceId: 1, field: 'status', fromValue: 'To Do', fromValueId: 1, @@ -137,7 +137,7 @@ describe('BusinessLogicService', () => { }, { issueId: 1, - datasetId: 1, + datasourceId: 1, field: 'status', fromValue: 'In Progress', fromValueId: 2, @@ -148,9 +148,9 @@ describe('BusinessLogicService', () => { ]; const statuses: Status[] = [ - {externalId: 1, category: StatusCategory.ToDo, dataSetId: 1, name: 'To Do'}, - {externalId: 2, category: StatusCategory.InProgress, dataSetId: 1, name: 'In Progress'}, - {externalId: 3, category: StatusCategory.Done, dataSetId: 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)); @@ -182,7 +182,7 @@ describe('BusinessLogicService', () => { id: 1, issueKey: 'ISSUE-1', title: 'Test Issue', - dataSetId: 1, + dataSourceId: 1, createdDate: new Date('2023-01-01'), status: 'Done', externalStatusId: 3, @@ -192,7 +192,7 @@ describe('BusinessLogicService', () => { const issueHistories: IssueHistory[] = [ { issueId: 1, - datasetId: 1, + datasourceId: 1, field: 'status', fromValue: 'To Do', fromValueId: 1, @@ -202,7 +202,7 @@ describe('BusinessLogicService', () => { }, { issueId: 1, - datasetId: 1, + datasourceId: 1, field: 'status', fromValue: 'In Progress', fromValueId: 2, @@ -212,7 +212,7 @@ describe('BusinessLogicService', () => { }, { issueId: 1, - datasetId: 1, + datasourceId: 1, field: 'status', fromValue: 'Done', fromValueId: 3, @@ -222,7 +222,7 @@ describe('BusinessLogicService', () => { }, { issueId: 1, - datasetId: 1, + datasourceId: 1, field: 'status', fromValue: 'In Progress', fromValueId: 2, @@ -233,9 +233,9 @@ describe('BusinessLogicService', () => { ]; const statuses: Status[] = [ - {externalId: 1, category: StatusCategory.ToDo, dataSetId: 1, name: 'To Do'}, - {externalId: 2, category: StatusCategory.InProgress, dataSetId: 1, name: 'In Progress'}, - {externalId: 3, category: StatusCategory.Done, dataSetId: 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)); diff --git a/src/app/services/business-logic.service.ts b/src/app/services/business-logic.service.ts index c262b24..149193d 100644 --- a/src/app/services/business-logic.service.ts +++ b/src/app/services/business-logic.service.ts @@ -34,7 +34,7 @@ export class BusinessLogicService { // Add a separate IssueHistory entry for the issue with createdDate const issueCreatedHistory: IssueHistory = { issueId: issue.id!, - datasetId: issue.dataSetId, + datasourceId: issue.dataSourceId, fromValue: '', toValueId: Number.parseInt(findStatusHistory.items[0].from), toValue: firstStatusChange.items[0].fromString, @@ -54,7 +54,7 @@ export class BusinessLogicService { }) => { const issueHistory: IssueHistory = { issueId: issue.id!, - datasetId: issue.dataSetId, + datasourceId: issue.dataSourceId, fromValue: item.fromString || '', fromValueId: Number.parseInt(item.from), toValueId: Number.parseInt(item.to), @@ -123,7 +123,7 @@ export class BusinessLogicService { issues.forEach(issue => { const status: Status = { - dataSetId: issue.dataSetId, + dataSourceId: issue.dataSourceId, name: issue.status, externalId: issue.externalStatusId }; @@ -135,9 +135,13 @@ export class BusinessLogicService { if (history.field === 'status') { if (history.fromValueId && !this.stateExistsInSet(statuses, history.fromValueId!)) { - statuses.push({dataSetId: history.datasetId, name: history.fromValue, externalId: history.fromValueId!}); + statuses.push({ + dataSourceId: history.datasourceId, + name: history.fromValue, + externalId: history.fromValueId! + }); } else if (history.toValueId && !this.stateExistsInSet(statuses, history.toValueId!)) { - statuses.push({dataSetId: history.datasetId, name: history.toValue, externalId: history.toValueId!}); + statuses.push({dataSourceId: history.datasourceId, name: history.toValue, externalId: history.toValueId!}); } } }); diff --git a/src/app/services/jira-cloud.service.ts b/src/app/services/jira-cloud.service.ts index 6475f6e..ee18536 100644 --- a/src/app/services/jira-cloud.service.ts +++ b/src/app/services/jira-cloud.service.ts @@ -8,7 +8,7 @@ import {ActivatedRoute, Router} from "@angular/router"; import {environment} from "../../environments/environment"; import {firstValueFrom} from "rxjs"; import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; -import {CALLBACK_JIRA_CLOUD, DASHBOARD, MANAGE_DATASETS} from "../app-routing.module"; +import {CALLBACK_JIRA_CLOUD, DASHBOARD, DATASOURCE_LIST} from "../app-routing.module"; import {BusinessLogicService} from "./business-logic.service"; /* @@ -77,29 +77,29 @@ export class JiraCloudService implements OnInit { const resourceId = resourceResponse[0].id; const appSettings = await this.storageService.getAppSettings(); - const dataset = await this.storageService.getDataset(appSettings.selectedDatasetId); - dataset.access_token = accessToken; - dataset.cloudId = resourceId; - await this.storageService.updateDataset(dataset); + const datasource = await this.storageService.getDatasource(appSettings.selectedDatasourceId); + datasource.access_token = accessToken; + datasource.cloudId = resourceId; + await this.storageService.updateDatasource(datasource); this.toastr.success('Successfully logged in to Jira'); - await this.router.navigate([MANAGE_DATASETS, appSettings.selectedDatasetId]); + await this.router.navigate([DATASOURCE_LIST, appSettings.selectedDatasourceId]); } - async getAndSaveIssues(dataSetId: number): Promise { - const dataset = await this.storageService.getDataset(dataSetId); + async getAndSaveIssues(dataSourceId: number): Promise { + const datasource = await this.storageService.getDatasource(dataSourceId); - if (dataset !== null && dataset?.access_token) { + if (datasource !== null && datasource?.access_token) { const client = new Version3Client({ - host: `https://api.atlassian.com/ex/jira/${dataset?.cloudId}`, + host: `https://api.atlassian.com/ex/jira/${datasource?.cloudId}`, authentication: { oauth2: { - accessToken: dataset.access_token!, + accessToken: datasource.access_token!, }, }, }); try { const response = await client.issueSearch.searchForIssuesUsingJqlPost({ - jql: dataset.jql, + jql: datasource.jql, fields: ['status', 'created', 'summary', 'issueType', 'statuscategorychangedate'], expand: ['changelog', 'names'], }); @@ -113,7 +113,7 @@ export class JiraCloudService implements OnInit { let i: Issue = { issueKey: issue.key, title: issue.fields.summary, - dataSetId: dataset.id!, + dataSourceId: datasource.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/jira-data-center.service.ts b/src/app/services/jira-data-center.service.ts index 736d7af..42b2c8f 100644 --- a/src/app/services/jira-data-center.service.ts +++ b/src/app/services/jira-data-center.service.ts @@ -64,7 +64,7 @@ export class JiraDataCenterService implements OnInit { async handleCallback(code: string): Promise { const appSettings = await this.databaseService.getAppSettings(); - const dataset = await this.databaseService.getDataset(appSettings.selectedDatasetId); + const datasource = await this.databaseService.getDatasource(appSettings.selectedDatasourceId); const body = new HttpParams() .set('grant_type', 'authorization_code') @@ -73,7 +73,7 @@ export class JiraDataCenterService implements OnInit { .set('code', code) .set('redirect_uri', this.redirectUri); - const tokenUrl = dataset.baseUrl + '/rest/oauth2/latest/token'; + const tokenUrl = datasource.baseUrl + '/rest/oauth2/latest/token'; const tokenResponse : any = await firstValueFrom(this.http.post(tokenUrl, body.toString(), { @@ -82,14 +82,14 @@ export class JiraDataCenterService implements OnInit { }), })); - dataset.access_token = tokenResponse.access_token; - await this.databaseService.updateDataset(dataset); + datasource.access_token = tokenResponse.access_token; + await this.databaseService.updateDatasource(datasource); this.router.navigate([DASHBOARD]); } async getIssues(dataSetId: number): Promise { - const config = await this.databaseService.getDataset(dataSetId); + const config = await this.databaseService.getDatasource(dataSetId); 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, - dataSetId: 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 1eb042b..358f556 100644 --- a/src/app/services/storage.service.ts +++ b/src/app/services/storage.service.ts @@ -1,5 +1,5 @@ import {Injectable} from '@angular/core'; -import {Dataset} from '../models/dataset'; +import {Datasource} from '../models/datasource'; import {Issue} from "../models/issue"; import {WorkItemAgeEntry} from "../models/workItemAgeEntry"; import {NgxIndexedDBModule, DBConfig, NgxIndexedDBService} from 'ngx-indexed-db'; @@ -12,29 +12,26 @@ import {ThroughputEntry} from "../models/throughputEntry"; import {CanceledCycleEntry} from "../models/canceledCycleEntry"; export class TableNames { - static readonly DATASETS = 'datasets'; + static readonly DATASOURCES = 'datasources'; static readonly ISSUES = 'issues'; - static readonly WORK_ITEM_AGE = 'workItemAge'; + static readonly WORK_ITEM_AGE = 'workItemAges'; static readonly APP_SETTINGS = 'appSettings'; - static readonly ISSUE_HISTORY = 'issueHistory'; - static readonly CYCLE_TIME = 'cycleTime'; + static readonly ISSUE_HISTORY = 'issueHistories'; + static readonly CYCLE_TIME = 'cycleTimes'; static readonly STATUS = 'status'; - static readonly THROUGHPUT = 'throughput'; - static readonly CANCELED_CYCLE = 'canceledCycle'; + static readonly THROUGHPUT = 'throughputs'; + static readonly CANCELED_CYCLE = 'canceledCycles'; } -export const dataSetDbConfig: DBConfig = { - name: 'metriqs-database-datasets', +export const dbConfigCore: DBConfig = { + name: 'metriqs-database-core', version: 1, migrationFactory: migrationFactoryDataset, objectStoresMeta: [{ - store: TableNames.DATASETS, + store: TableNames.DATASOURCES, storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ {name: 'url', keypath: 'url', options: {unique: false}}, - {name: 'access_token', keypath: 'access_token', options: {unique: false}}, - {name: 'cloudId', keypath: 'cloudId', options: {unique: false}}, - {name: 'jql', keypath: 'jql', options: {unique: false}}, ] }, { store: TableNames.APP_SETTINGS, @@ -56,7 +53,7 @@ export const dbConfigIssueData: DBConfig = { objectStoresMeta: [ { store: TableNames.ISSUES, storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ - {name: 'dataSetId', keypath: 'dataSetId', options: { unique: false}}, + {name: 'dataSourceId', keypath: 'dataSourceId', options: {unique: false}}, {name: 'issueKey', keypath: 'issueKey', options: {unique: false}}, {name: 'id', keypath: 'id', options: {unique: false}}, ] @@ -114,7 +111,7 @@ export function migrationFactoryDataset() { // 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.DATASETS); + const issues = transaction.objectStore(TableNames.DATASOURCES); const settings = transaction.objectStore(TableNames.APP_SETTINGS); const status = transaction.objectStore(TableNames.STATUS); }, @@ -141,24 +138,24 @@ export class StorageService { }); } - async addDataset(dataset: Dataset): Promise { - this.dbService.selectDb(dataSetDbConfig.name); - return firstValueFrom(this.dbService.add(TableNames.DATASETS, dataset)); + async addDataset(dataset: Datasource): Promise { + this.dbService.selectDb(dbConfigCore.name); + return firstValueFrom(this.dbService.add(TableNames.DATASOURCES, dataset)); } - async getAllDatasets(): Promise { - this.dbService.selectDb(dataSetDbConfig.name); - return firstValueFrom(this.dbService.getAll(TableNames.DATASETS)); + async getAllDatasources(): Promise { + this.dbService.selectDb(dbConfigCore.name); + return firstValueFrom(this.dbService.getAll(TableNames.DATASOURCES)); } - async removeDataset(id: number): Promise { - this.dbService.selectDb(dataSetDbConfig.name); - await firstValueFrom(this.dbService.delete(TableNames.DATASETS, id)); + async removeDatasource(id: number): Promise { + this.dbService.selectDb(dbConfigCore.name); + await firstValueFrom(this.dbService.delete(TableNames.DATASOURCES, id)); } - async updateDataset(dataset: Dataset): Promise { - this.dbService.selectDb(dataSetDbConfig.name); - return await firstValueFrom(this.dbService.update(TableNames.DATASETS, dataset)); + async updateDatasource(dataset: Datasource): Promise { + this.dbService.selectDb(dbConfigCore.name); + return await firstValueFrom(this.dbService.update(TableNames.DATASOURCES, dataset)); } async hasWorkItemAgeData(): Promise { @@ -191,24 +188,24 @@ export class StorageService { return firstValueFrom(this.dbService.bulkAdd(TableNames.WORK_ITEM_AGE, workItemAgeEntries)); } - async createDataset(newDataset: Dataset) { - this.dbService.selectDb(dataSetDbConfig.name); - return firstValueFrom(this.dbService.add(TableNames.DATASETS, newDataset)); + async createDataset(newDataset: Datasource) { + this.dbService.selectDb(dbConfigCore.name); + return firstValueFrom(this.dbService.add(TableNames.DATASOURCES, newDataset)); } - getDataset(id: number): Promise { - this.dbService.selectDb(dataSetDbConfig.name); - return firstValueFrom(this.dbService.getByID(TableNames.DATASETS, id)); + getDatasource(id: number): Promise { + this.dbService.selectDb(dbConfigCore.name); + return firstValueFrom(this.dbService.getByID(TableNames.DATASOURCES, id)); } async saveAppSettings(appSettings: AppSettings): Promise { - this.dbService.selectDb(dataSetDbConfig.name); + this.dbService.selectDb(dbConfigCore.name); await firstValueFrom(this.dbService.clear(TableNames.APP_SETTINGS)); return firstValueFrom(this.dbService.add(TableNames.APP_SETTINGS, appSettings)); } async getAppSettings(): Promise { - this.dbService.selectDb(dataSetDbConfig.name); + this.dbService.selectDb(dbConfigCore.name); return firstValueFrom(this.dbService.getAll(TableNames.APP_SETTINGS)).then(datasets => datasets[0]); } @@ -247,7 +244,7 @@ export class StorageService { } async addStatuses(status: Status[]) { - this.dbService.selectDb(dataSetDbConfig.name); + this.dbService.selectDb(dbConfigCore.name); return firstValueFrom(this.dbService.bulkAdd(TableNames.STATUS, status)); } @@ -263,12 +260,12 @@ export class StorageService { getAllStatuses() { - this.dbService.selectDb(dataSetDbConfig.name); + this.dbService.selectDb(dbConfigCore.name); return firstValueFrom(this.dbService.getAll(TableNames.STATUS)); } updateStatus(status: Status) { - this.dbService.selectDb(dataSetDbConfig.name); + this.dbService.selectDb(dbConfigCore.name); return firstValueFrom(this.dbService.update(TableNames.STATUS, status)); } From 068ebfc47880b1a2af5e31ba9b3d0b26fbe7260a Mon Sep 17 00:00:00 2001 From: mario meltzow Date: Tue, 20 Aug 2024 18:44:20 +0200 Subject: [PATCH 4/4] add work in progress chart --- src/app/app-routing.module.ts | 3 + .../components/layout/layout.component.html | 2 +- src/app/components/layout/layout.component.ts | 10 ++- .../work-in-progress-page.component.html | 7 ++ .../work-in-progress-page.component.scss | 0 .../work-in-progress-page.component.spec.ts | 23 ++++++ .../work-in-progress-page.component.ts | 80 +++++++++++++++++++ src/app/models/workInProgressEntry.ts | 2 +- src/app/services/business-logic.service.ts | 36 +++++++++ src/app/services/jira-cloud.service.ts | 3 + src/app/services/storage.service.ts | 26 ++++-- 11 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 src/app/components/work-in-progress/work-in-progress-page.component.html create mode 100644 src/app/components/work-in-progress/work-in-progress-page.component.scss create mode 100644 src/app/components/work-in-progress/work-in-progress-page.component.spec.ts create mode 100644 src/app/components/work-in-progress/work-in-progress-page.component.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 7dbc25e..d49bb23 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -8,6 +8,7 @@ import {CallbackComponent} from "./components/callback.component"; import {CycleTimePage} from "./components/cycle-time-page/cycle-time.page"; import {DatasourceEditComponent} from "./components/datasource/datasource-edit.component"; import {ThroughputPageComponent} from "./components/throughput-page/throughput-page.component"; +import {WorkInProgressPageComponent} from "./components/work-in-progress/work-in-progress-page.component"; export const CALLBACK_JIRA_CLOUD = 'callbackJiraCloud'; export const CALLBACK_JIRA_DATA_CENTER = 'callbackJiraDataCenter'; @@ -17,6 +18,7 @@ export const CYCLE_TIME = 'cycle-time'; export const DATASOURCE_LIST = 'datasources'; export const DATASOURCE_CREATE = DATASOURCE_LIST + '/create'; export const THROUGHPUT = 'throughput'; +export const WORK_IN_PROGRESS = 'work-in-progress'; export const routes: Routes = [ {path: '', redirectTo: `/${DASHBOARD}`, pathMatch: 'full'}, @@ -29,6 +31,7 @@ export const routes: Routes = [ {path: DATASOURCE_LIST, component: DatasourceListComponent}, {path: DATASOURCE_LIST + '/:id', component: DatasourceEditComponent}, {path: DATASOURCE_CREATE, component: DatasourceEditComponent}, + {path: WORK_IN_PROGRESS, component: WorkInProgressPageComponent} ]; @NgModule({ diff --git a/src/app/components/layout/layout.component.html b/src/app/components/layout/layout.component.html index 5607938..93e919f 100644 --- a/src/app/components/layout/layout.component.html +++ b/src/app/components/layout/layout.component.html @@ -9,7 +9,7 @@ Work Item Age Cycle Time Throughput - WIP + Work in Progress Monte Carlo Simulation diff --git a/src/app/components/layout/layout.component.ts b/src/app/components/layout/layout.component.ts index b9a4cf5..2ed2e57 100644 --- a/src/app/components/layout/layout.component.ts +++ b/src/app/components/layout/layout.component.ts @@ -15,7 +15,14 @@ import {MatFormFieldModule} from "@angular/material/form-field"; import {StorageService} from "../../services/storage.service"; import {Datasource, DataSourceType} from "../../models/datasource"; import {ToastrService} from "ngx-toastr"; -import {CYCLE_TIME, DASHBOARD, DATASOURCE_LIST, THROUGHPUT, WORK_ITEM_AGE} from "../../app-routing.module"; +import { + CYCLE_TIME, + DASHBOARD, + DATASOURCE_LIST, + THROUGHPUT, + WORK_IN_PROGRESS, + WORK_ITEM_AGE +} from "../../app-routing.module"; import {JiraDataCenterService} from "../../services/jira-data-center.service"; import {JiraCloudService} from "../../services/jira-cloud.service"; import {WorkItemAgeChartComponent} from "../work-item-age-chart/work-item-age-chart.component"; @@ -149,4 +156,5 @@ export class LayoutComponent implements OnInit { } } + protected readonly WORK_IN_PROGRESS = WORK_IN_PROGRESS; } diff --git a/src/app/components/work-in-progress/work-in-progress-page.component.html b/src/app/components/work-in-progress/work-in-progress-page.component.html new file mode 100644 index 0000000..93c2005 --- /dev/null +++ b/src/app/components/work-in-progress/work-in-progress-page.component.html @@ -0,0 +1,7 @@ +
+ + +
diff --git a/src/app/components/work-in-progress/work-in-progress-page.component.scss b/src/app/components/work-in-progress/work-in-progress-page.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/work-in-progress/work-in-progress-page.component.spec.ts b/src/app/components/work-in-progress/work-in-progress-page.component.spec.ts new file mode 100644 index 0000000..bd07a50 --- /dev/null +++ b/src/app/components/work-in-progress/work-in-progress-page.component.spec.ts @@ -0,0 +1,23 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {WorkInProgressPageComponent} from './work-in-progress-page.component'; + +describe('WorkInProgressPageComponent', () => { + let component: WorkInProgressPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [WorkInProgressPageComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(WorkInProgressPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000..d437177 --- /dev/null +++ b/src/app/components/work-in-progress/work-in-progress-page.component.ts @@ -0,0 +1,80 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {BaseChartDirective} from 'ng2-charts'; +import {ChartConfiguration, ChartType} from 'chart.js'; +import {StorageService} from '../../services/storage.service'; +import {ThroughputEntry} from '../../models/throughputEntry'; +import {WorkInProgressEntry} from "../../models/workInProgressEntry"; + +@Component({ + selector: 'app-work-in-progress-page', + standalone: true, + imports: [CommonModule, BaseChartDirective], + templateUrl: './work-in-progress-page.component.html', + styleUrls: ['./work-in-progress-page.component.scss'] +}) +export class WorkInProgressPageComponent implements OnInit { + @ViewChild(BaseChartDirective) chart?: BaseChartDirective; + + public lineChartData: ChartConfiguration['data'] = { + datasets: [ + { + data: [], + label: 'Work In Progress', + backgroundColor: 'rgba(77,83,96,0.2)', + borderColor: 'rgba(77,83,96,1)', + pointBackgroundColor: 'rgba(77,83,96,1)', + pointBorderColor: '#fff', + pointHoverBackgroundColor: '#fff', + pointHoverBorderColor: 'rgba(77,83,96,0.8)', + fill: 'origin', + } + ], + labels: [] + }; + + public lineChartOptions: ChartConfiguration['options'] = { + responsive: true, + }; + + public lineChartType: ChartType = 'line'; + + constructor(private storageService: StorageService) { + } + + async ngOnInit() { + const wipData: WorkInProgressEntry[] = await this.storageService.getWorkInProgressData(); + const allDates = this.generateAllDates(wipData); + const mappedData = this.mapDataToDates(allDates, wipData); + + this.lineChartData.datasets[0].data = mappedData.map(entry => entry.wip); + this.lineChartData.labels = mappedData.map(entry => entry.date.toDateString()); + this.chart?.update(); + } + + private generateAllDates(data: WorkInProgressEntry[]): { date: Date, wip: number }[] { + if (data.length === 0) return []; + + const startDate = new Date(Math.min(...data.map(entry => entry.date.getTime()))); + const endDate = new Date(Math.max(...data.map(entry => entry.date.getTime()))); + const allDates = []; + + for (let date = new Date(startDate); date <= endDate; date.setDate(date.getDate() + 1)) { + allDates.push({date: new Date(date), wip: 0}); + } + + return allDates; + } + + private mapDataToDates(allDates: { date: Date, wip: number }[], data: WorkInProgressEntry[]): { + date: Date, + wip: number + }[] { + const dataMap = new Map(data.map(entry => [entry.date.toDateString(), entry.wip])); + + return allDates.map(entry => ({ + date: entry.date, + wip: dataMap.get(entry.date.toDateString()) || 0 + })); + } +} diff --git a/src/app/models/workInProgressEntry.ts b/src/app/models/workInProgressEntry.ts index 8bb79f5..1275e82 100644 --- a/src/app/models/workInProgressEntry.ts +++ b/src/app/models/workInProgressEntry.ts @@ -1,4 +1,4 @@ -export interface ThroughputEntry { +export interface WorkInProgressEntry { issueIds: number[]; wip: number; date: Date; diff --git a/src/app/services/business-logic.service.ts b/src/app/services/business-logic.service.ts index 149193d..da6975b 100644 --- a/src/app/services/business-logic.service.ts +++ b/src/app/services/business-logic.service.ts @@ -8,6 +8,7 @@ import {Status, StatusCategory} from "../models/status"; import {CanceledCycleEntry} from "../models/canceledCycleEntry"; import {ThroughputEntry} from "../models/throughputEntry"; import {count} from "rxjs"; +import {WorkInProgressEntry} from "../models/workInProgressEntry"; @Injectable({ providedIn: 'root' @@ -305,4 +306,39 @@ export class BusinessLogicService { return throughputEntries; } + + + async computeWorkInProgress(): Promise { + const issueHistories: IssueHistory[] = await this.storageService.getAllIssueHistories(); + const statuses: Status[] = await this.storageService.getAllStatuses(); + + const inProgressStatusIds = statuses + .filter(status => status.category === StatusCategory.InProgress) + .map(status => status.externalId); + + const workInProgressMap = new Map>(); + + issueHistories.forEach(history => { + const dateStr = history.createdDate.toDateString(); + if (!workInProgressMap.has(dateStr)) { + workInProgressMap.set(dateStr, new Set()); + } + if (inProgressStatusIds.includes(history.toValueId!)) { + workInProgressMap.get(dateStr)!.add(history.issueId); + } + }); + + const workInProgressEntries: WorkInProgressEntry[] = []; + workInProgressMap.forEach((issueIds, dateStr) => { + workInProgressEntries.push({ + date: new Date(dateStr), + wip: issueIds.size, + issueIds: Array.from(issueIds) + }); + }); + + + await this.storageService.saveWorkInProgressData(workInProgressEntries); + return workInProgressEntries; + } } diff --git a/src/app/services/jira-cloud.service.ts b/src/app/services/jira-cloud.service.ts index ee18536..0797d54 100644 --- a/src/app/services/jira-cloud.service.ts +++ b/src/app/services/jira-cloud.service.ts @@ -144,6 +144,9 @@ export class JiraCloudService implements OnInit { const throughputs = this.businessLogicService.findThroughputEntries(cycleTimeEntries); await this.storageService.saveThroughputData(throughputs); + const workInProgressEntries = this.businessLogicService.computeWorkInProgress(); + + return issues; } catch (error) { this.toastr.error('Failed to fetch issues from Jira', error!.toString()); diff --git a/src/app/services/storage.service.ts b/src/app/services/storage.service.ts index 358f556..c076083 100644 --- a/src/app/services/storage.service.ts +++ b/src/app/services/storage.service.ts @@ -10,6 +10,7 @@ import {CycleTimeEntry} from "../models/cycleTimeEntry"; import {Status} from "../models/status"; import {ThroughputEntry} from "../models/throughputEntry"; import {CanceledCycleEntry} from "../models/canceledCycleEntry"; +import {WorkInProgressEntry} from "../models/workInProgressEntry"; export class TableNames { static readonly DATASOURCES = 'datasources'; @@ -21,17 +22,17 @@ export class TableNames { static readonly STATUS = 'status'; static readonly THROUGHPUT = 'throughputs'; static readonly CANCELED_CYCLE = 'canceledCycles'; + static readonly WORK_IN_PROGRESS = 'workInProgress'; } export const dbConfigCore: DBConfig = { name: 'metriqs-database-core', version: 1, - migrationFactory: migrationFactoryDataset, + migrationFactory: migrationFactoryCore, objectStoresMeta: [{ store: TableNames.DATASOURCES, storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ - {name: 'url', keypath: 'url', options: {unique: false}}, ] }, { store: TableNames.APP_SETTINGS, @@ -49,7 +50,7 @@ export const dbConfigCore: DBConfig = { export const dbConfigIssueData: DBConfig = { name: 'metriqs-database-issue-data', version: 1, - migrationFactory: migrationFactory, + migrationFactory: migrationFactoryIssues, objectStoresMeta: [ { store: TableNames.ISSUES, storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ @@ -87,13 +88,16 @@ export const dbConfigIssueData: DBConfig = { storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [ {name: 'issueId', keypath: 'issueId', options: {unique: false}}, ] + }, { + store: TableNames.WORK_IN_PROGRESS, + storeConfig: {keyPath: 'id', autoIncrement: true}, storeSchema: [] }, ] }; // Ahead of time compiles requires an exported function for factories -export function migrationFactory() { +export function migrationFactoryIssues() { return { 1: (db: any, transaction: { objectStore: (arg0: string) => any; }) => { const issues = transaction.objectStore(TableNames.ISSUES); @@ -106,7 +110,7 @@ export function migrationFactory() { }; } -export function migrationFactoryDataset() { +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 { @@ -129,7 +133,7 @@ export class StorageService { async recreateDatabase(): Promise { return this.deleteIssueDatabase().then(async () => { for (const storeMeta of dbConfigIssueData.objectStoresMeta) { - const store = await this.dbService.createObjectStore(storeMeta, migrationFactory); + const store = await this.dbService.createObjectStore(storeMeta, migrationFactoryIssues); // storeMeta.storeSchema.forEach(schema => { // store.createIndex(schema.name, schema.keypath, schema.options); // }); @@ -296,4 +300,14 @@ export class StorageService { this.dbService.selectDb(dbConfigIssueData.name); return firstValueFrom(this.dbService.bulkAdd(TableNames.THROUGHPUT, througputs)); } + + async getWorkInProgressData() { + this.dbService.selectDb(dbConfigIssueData.name); + return firstValueFrom(this.dbService.getAll(TableNames.WORK_IN_PROGRESS)); + } + + saveWorkInProgressData(workInProgressEntries: WorkInProgressEntry[]) { + this.dbService.selectDb(dbConfigIssueData.name); + return firstValueFrom(this.dbService.bulkAdd(TableNames.WORK_IN_PROGRESS, workInProgressEntries)); + } }