diff --git a/.gitignore b/.gitignore index 1dce6ce..43607ab 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ configs/redmine-status-changes-config.jsonc configs/eccm-versions-config.jsonc configs/eccm-config.jsonc configs/current-user-rules.jsonc +configs/simple-kanban-board-config.jsonc +configs/kanban-boards/ diff --git a/configs/simple-kanban-board-config.jsonc.dist b/configs/simple-kanban-board-config.jsonc.dist new file mode 100644 index 0000000..d1f1c8a --- /dev/null +++ b/configs/simple-kanban-board-config.jsonc.dist @@ -0,0 +1,3 @@ +{ + "path": "" +} \ No newline at end of file diff --git a/libs/event-emitter/src/configs/dynamic-loader.ts b/libs/event-emitter/src/configs/dynamic-loader.ts new file mode 100644 index 0000000..b357d00 --- /dev/null +++ b/libs/event-emitter/src/configs/dynamic-loader.ts @@ -0,0 +1,27 @@ +import { CacheTTL, Injectable, Logger } from '@nestjs/common'; +import { readFileSync, existsSync } from 'fs'; +import { join } from 'path'; + +@Injectable() +export class DynamicLoader { + private logger = new Logger(DynamicLoader.name); + + @CacheTTL(60) + load( + file: string, + opts: { path: string; ext: string; parser: (raw: any) => any }, + ): any { + const fullFileName = join(opts.path, `${file}.${opts.ext}`); + if (!existsSync(fullFileName)) return null; + let rawData: any; + let data: any; + try { + rawData = readFileSync(fullFileName, { encoding: 'utf-8' }); + data = opts.parser(rawData); + } catch (ex) { + this.logger.error(`Error at config read - ${ex}`); + return null; + } + return data; + } +} diff --git a/libs/event-emitter/src/event-emitter.module.ts b/libs/event-emitter/src/event-emitter.module.ts index d99c5c0..ce4c1cf 100644 --- a/libs/event-emitter/src/event-emitter.module.ts +++ b/libs/event-emitter/src/event-emitter.module.ts @@ -20,6 +20,7 @@ import { TimestampEnhancer } from './issue-enhancers/timestamps-enhancer'; import { EnhancerService } from './issue-enhancers/enhancer.service'; import { ProjectDashboardService } from './project-dashboard/project-dashboard.service'; import { RootIssueSubTreesWidgetService } from './project-dashboard/widgets/root-issue-subtrees.widget.service'; +import { DynamicLoader } from './configs/dynamic-loader'; @Module({}) export class EventEmitterModule implements OnModuleInit { @@ -44,6 +45,7 @@ export class EventEmitterModule implements OnModuleInit { EnhancerService, ProjectDashboardService, RootIssueSubTreesWidgetService, + DynamicLoader, ], exports: [ EventEmitterService, @@ -60,6 +62,7 @@ export class EventEmitterModule implements OnModuleInit { EnhancerService, ProjectDashboardService, RootIssueSubTreesWidgetService, + DynamicLoader, ], controllers: [MainController, UsersController, IssuesController], }; diff --git a/package-lock.json b/package-lock.json index 63fd4b1..1c17d5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "handlebars": "^4.7.7", "hbs": "^4.2.0", "imap-simple": "^5.1.0", + "jsonc-parser": "^3.2.0", "luxon": "^3.1.0", "nano": "^10.0.0", "node-telegram-bot-api": "^0.59.0", @@ -198,6 +199,12 @@ "node": ">=8.0.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, "node_modules/@angular-devkit/schematics/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -1707,6 +1714,12 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, "node_modules/@nestjs/schematics/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -7091,10 +7104,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", - "dev": true + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "node_modules/jsonfile": { "version": "6.1.0", @@ -10448,6 +10460,12 @@ "rxjs": "6.6.7" }, "dependencies": { + "jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, "rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -11625,6 +11643,12 @@ "rxjs": "6.6.7" } }, + "jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, "rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -15740,10 +15764,9 @@ "dev": true }, "jsonc-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", - "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", - "dev": true + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "jsonfile": { "version": "6.1.0", diff --git a/package.json b/package.json index c2455af..a871bf2 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "handlebars": "^4.7.7", "hbs": "^4.2.0", "imap-simple": "^5.1.0", + "jsonc-parser": "^3.2.0", "luxon": "^3.1.0", "nano": "^10.0.0", "node-telegram-bot-api": "^0.59.0", diff --git a/src/app.module.ts b/src/app.module.ts index 810ba7f..950481f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -36,7 +36,7 @@ import { DailyEccmReportsUserCommentsDatasource } from './couchdb-datasources/da import { DailyEccmUserCommentsService } from './reports/daily-eccm-user-comments.service'; import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handlers/set-daily-eccm-user-comment.bot-handler.service'; import { DailyEccmWithExtraDataService } from './reports/daily-eccm-with-extra-data.service'; -import { Eccm110DashboardController } from './dashboards/eccm110-dashboard.controller'; +import { SimpleKanbanBoardController } from './dashboards/simple-kanban-board.controller'; @Module({ imports: [ @@ -54,7 +54,7 @@ import { Eccm110DashboardController } from './dashboards/eccm110-dashboard.contr MainController, CurrentIssuesEccmReportController, DailyEccmReportController, - Eccm110DashboardController, + SimpleKanbanBoardController, ], providers: [ AppService, diff --git a/src/configs/app.ts b/src/configs/app.ts index 9919b6c..89c3e6c 100644 --- a/src/configs/app.ts +++ b/src/configs/app.ts @@ -7,12 +7,14 @@ import RedmineStatusesConfigLoader from './statuses.config'; import RedmineStatusChangesConfigLoader from './status-changes.config'; import RedmineEccmConfig from './eccm.config'; import RedmineCurrentUserRulesConfig from './current-user-rules.config'; +import SimpleKanbanBoardConfig from './simple-kanban-board.config'; const redmineIssueEventEmitterConfig = RedmineIssueEventEmitterConfigLoader(); const redmineStatusesConfig = RedmineStatusesConfigLoader(); const redmineStatusChanges = RedmineStatusChangesConfigLoader(); const redmineEccm = RedmineEccmConfig(); const redmineCurrentUserRules = RedmineCurrentUserRulesConfig(); +const simpleKanbanBoard = SimpleKanbanBoardConfig(); let appConfig: AppConfig; @@ -36,6 +38,7 @@ export default (): AppConfig => { redmineStatusChanges: redmineStatusChanges, redmineEccm: redmineEccm, redmineCurrentUserRules: redmineCurrentUserRules, + simpleKanbanBoard: simpleKanbanBoard, }; return appConfig; diff --git a/src/configs/simple-kanban-board.config.ts b/src/configs/simple-kanban-board.config.ts new file mode 100644 index 0000000..311a20c --- /dev/null +++ b/src/configs/simple-kanban-board.config.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { parse } from 'jsonc-parser'; + +export type SimpleKanbanBoardConfig = { + path: string; +}; + +let config: SimpleKanbanBoardConfig; + +export default (): SimpleKanbanBoardConfig => { + if (config) { + return config; + } + + const userDefinedConfigPath = + process.env['ELTEX_REDMINE_HELPER_KANBAN_SIMPLE_BOARD_CONFIG_PATH']; + const defaultConfigPath = join('configs', 'simple-kanban-board-config.jsonc'); + + const configPath = userDefinedConfigPath || defaultConfigPath; + + const rawData = readFileSync(configPath, { encoding: 'utf-8' }); + + config = parse(rawData); + + return config; +}; diff --git a/src/dashboards/eccm110-dashboard.controller.ts b/src/dashboards/eccm110-dashboard.controller.ts deleted file mode 100644 index 09eaddd..0000000 --- a/src/dashboards/eccm110-dashboard.controller.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { RootIssueSubTreesWidgetService } from '@app/event-emitter/project-dashboard/widgets/root-issue-subtrees.widget.service'; -import { Controller, Get } from '@nestjs/common'; - -@Controller('eccm-1.10-dashboard') -export class Eccm110DashboardController { - constructor( - private rootIssueSubTreesWidgetService: RootIssueSubTreesWidgetService, - ) {} - - // TODO: code for Eccm110DashboardController - - @Get('/raw') - async getRawData(): Promise { - return await this.rootIssueSubTreesWidgetService.render({ - rootIssueId: 2, - parentsAsGroups: true, - statuses: [ - 'New', - 'Closed', - 'In Progress', - 'Re-opened', - 'Code Review', - 'Resolved', - 'Testing', - 'Wait Release', - 'Pending', - 'Feedback', - 'Rejected', - ], - }); - } -} diff --git a/src/dashboards/simple-kanban-board.controller.ts b/src/dashboards/simple-kanban-board.controller.ts new file mode 100644 index 0000000..51309a9 --- /dev/null +++ b/src/dashboards/simple-kanban-board.controller.ts @@ -0,0 +1,34 @@ +import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader'; +import { RootIssueSubTreesWidgetService } from '@app/event-emitter/project-dashboard/widgets/root-issue-subtrees.widget.service'; +import { Controller, Get, Param, Render } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { parse } from 'jsonc-parser'; + +@Controller('simple-kanban-board') +export class SimpleKanbanBoardController { + private path: string; + + constructor( + private rootIssueSubTreesWidgetService: RootIssueSubTreesWidgetService, + private dynamicLoader: DynamicLoader, + private configService: ConfigService, + ) { + this.path = this.configService.get('simpleKanbanBoard.path'); + } + + @Get('/tree/:name/raw') + async getRawData(@Param('name') name: string): Promise { + const cfg = this.dynamicLoader.load(name, { + path: this.path, + ext: 'jsonc', + parser: parse, + }); + return await this.rootIssueSubTreesWidgetService.render(cfg); + } + + @Get('/tree/:name') + @Render('simple-kanban-board') + async get(@Param('name') name: string): Promise { + return await this.getRawData(name); + } +} diff --git a/src/models/app-config.model.ts b/src/models/app-config.model.ts index 141d191..4ff91ba 100644 --- a/src/models/app-config.model.ts +++ b/src/models/app-config.model.ts @@ -1,4 +1,5 @@ import { MainConfigModel } from '@app/event-emitter/models/main-config-model'; +import { SimpleKanbanBoardConfig } from 'src/configs/simple-kanban-board.config'; import { EccmConfig } from './eccm-config.model'; import { StatusChangesConfig } from './status-changes-config.model'; import { StatusesConfig } from './statuses-config.model'; @@ -9,6 +10,7 @@ export type AppConfig = { redmineStatusChanges: StatusChangesConfig.Config; redmineEccm: EccmConfig.Config; redmineCurrentUserRules: Record; + simpleKanbanBoard: SimpleKanbanBoardConfig; couchDb: { dbs: { changes: string; diff --git a/views/simple-kanban-board.hbs b/views/simple-kanban-board.hbs new file mode 100644 index 0000000..d391d30 --- /dev/null +++ b/views/simple-kanban-board.hbs @@ -0,0 +1,66 @@ + + + + + + ECCM 1.10 Kanban board + + + + + {{#each this}} +

{{this.metainfo.title}}

+
+ {{#each this.data}} +
+
{{this.status}}
+ {{#each this.issues}} +
+
{{this.tracker.name}} #{{this.id}} - {{this.subject}}
+
Исп.: {{this.current_user.name}}
+
Прогресс: {{this.done_ration}}
+
Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}
+
+ {{/each}} +
+ {{/each}} +
+ {{/each}} + + \ No newline at end of file