Добавлена динамическая загрузка досок kanban

This commit is contained in:
Pavel Gnedov 2023-02-07 13:04:49 +07:00
parent c3ad1df1b6
commit 99b31164cf
13 changed files with 201 additions and 42 deletions

2
.gitignore vendored
View file

@ -41,3 +41,5 @@ configs/redmine-status-changes-config.jsonc
configs/eccm-versions-config.jsonc configs/eccm-versions-config.jsonc
configs/eccm-config.jsonc configs/eccm-config.jsonc
configs/current-user-rules.jsonc configs/current-user-rules.jsonc
configs/simple-kanban-board-config.jsonc
configs/kanban-boards/

View file

@ -0,0 +1,3 @@
{
"path": ""
}

View file

@ -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;
}
}

View file

@ -20,6 +20,7 @@ import { TimestampEnhancer } from './issue-enhancers/timestamps-enhancer';
import { EnhancerService } from './issue-enhancers/enhancer.service'; import { EnhancerService } from './issue-enhancers/enhancer.service';
import { ProjectDashboardService } from './project-dashboard/project-dashboard.service'; import { ProjectDashboardService } from './project-dashboard/project-dashboard.service';
import { RootIssueSubTreesWidgetService } from './project-dashboard/widgets/root-issue-subtrees.widget.service'; import { RootIssueSubTreesWidgetService } from './project-dashboard/widgets/root-issue-subtrees.widget.service';
import { DynamicLoader } from './configs/dynamic-loader';
@Module({}) @Module({})
export class EventEmitterModule implements OnModuleInit { export class EventEmitterModule implements OnModuleInit {
@ -44,6 +45,7 @@ export class EventEmitterModule implements OnModuleInit {
EnhancerService, EnhancerService,
ProjectDashboardService, ProjectDashboardService,
RootIssueSubTreesWidgetService, RootIssueSubTreesWidgetService,
DynamicLoader,
], ],
exports: [ exports: [
EventEmitterService, EventEmitterService,
@ -60,6 +62,7 @@ export class EventEmitterModule implements OnModuleInit {
EnhancerService, EnhancerService,
ProjectDashboardService, ProjectDashboardService,
RootIssueSubTreesWidgetService, RootIssueSubTreesWidgetService,
DynamicLoader,
], ],
controllers: [MainController, UsersController, IssuesController], controllers: [MainController, UsersController, IssuesController],
}; };

39
package-lock.json generated
View file

@ -22,6 +22,7 @@
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"hbs": "^4.2.0", "hbs": "^4.2.0",
"imap-simple": "^5.1.0", "imap-simple": "^5.1.0",
"jsonc-parser": "^3.2.0",
"luxon": "^3.1.0", "luxon": "^3.1.0",
"nano": "^10.0.0", "nano": "^10.0.0",
"node-telegram-bot-api": "^0.59.0", "node-telegram-bot-api": "^0.59.0",
@ -198,6 +199,12 @@
"node": ">=8.0.0" "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": { "node_modules/@angular-devkit/schematics/node_modules/rxjs": {
"version": "6.6.7", "version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
@ -1707,6 +1714,12 @@
"yarn": ">= 1.13.0" "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": { "node_modules/@nestjs/schematics/node_modules/rxjs": {
"version": "6.6.7", "version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
@ -7091,10 +7104,9 @@
} }
}, },
"node_modules/jsonc-parser": { "node_modules/jsonc-parser": {
"version": "3.0.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="
"dev": true
}, },
"node_modules/jsonfile": { "node_modules/jsonfile": {
"version": "6.1.0", "version": "6.1.0",
@ -10448,6 +10460,12 @@
"rxjs": "6.6.7" "rxjs": "6.6.7"
}, },
"dependencies": { "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": { "rxjs": {
"version": "6.6.7", "version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
@ -11625,6 +11643,12 @@
"rxjs": "6.6.7" "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": { "rxjs": {
"version": "6.6.7", "version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
@ -15740,10 +15764,9 @@
"dev": true "dev": true
}, },
"jsonc-parser": { "jsonc-parser": {
"version": "3.0.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
"integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w=="
"dev": true
}, },
"jsonfile": { "jsonfile": {
"version": "6.1.0", "version": "6.1.0",

View file

@ -34,6 +34,7 @@
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"hbs": "^4.2.0", "hbs": "^4.2.0",
"imap-simple": "^5.1.0", "imap-simple": "^5.1.0",
"jsonc-parser": "^3.2.0",
"luxon": "^3.1.0", "luxon": "^3.1.0",
"nano": "^10.0.0", "nano": "^10.0.0",
"node-telegram-bot-api": "^0.59.0", "node-telegram-bot-api": "^0.59.0",

View file

@ -36,7 +36,7 @@ import { DailyEccmReportsUserCommentsDatasource } from './couchdb-datasources/da
import { DailyEccmUserCommentsService } from './reports/daily-eccm-user-comments.service'; import { DailyEccmUserCommentsService } from './reports/daily-eccm-user-comments.service';
import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handlers/set-daily-eccm-user-comment.bot-handler.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 { 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({ @Module({
imports: [ imports: [
@ -54,7 +54,7 @@ import { Eccm110DashboardController } from './dashboards/eccm110-dashboard.contr
MainController, MainController,
CurrentIssuesEccmReportController, CurrentIssuesEccmReportController,
DailyEccmReportController, DailyEccmReportController,
Eccm110DashboardController, SimpleKanbanBoardController,
], ],
providers: [ providers: [
AppService, AppService,

View file

@ -7,12 +7,14 @@ import RedmineStatusesConfigLoader from './statuses.config';
import RedmineStatusChangesConfigLoader from './status-changes.config'; import RedmineStatusChangesConfigLoader from './status-changes.config';
import RedmineEccmConfig from './eccm.config'; import RedmineEccmConfig from './eccm.config';
import RedmineCurrentUserRulesConfig from './current-user-rules.config'; import RedmineCurrentUserRulesConfig from './current-user-rules.config';
import SimpleKanbanBoardConfig from './simple-kanban-board.config';
const redmineIssueEventEmitterConfig = RedmineIssueEventEmitterConfigLoader(); const redmineIssueEventEmitterConfig = RedmineIssueEventEmitterConfigLoader();
const redmineStatusesConfig = RedmineStatusesConfigLoader(); const redmineStatusesConfig = RedmineStatusesConfigLoader();
const redmineStatusChanges = RedmineStatusChangesConfigLoader(); const redmineStatusChanges = RedmineStatusChangesConfigLoader();
const redmineEccm = RedmineEccmConfig(); const redmineEccm = RedmineEccmConfig();
const redmineCurrentUserRules = RedmineCurrentUserRulesConfig(); const redmineCurrentUserRules = RedmineCurrentUserRulesConfig();
const simpleKanbanBoard = SimpleKanbanBoardConfig();
let appConfig: AppConfig; let appConfig: AppConfig;
@ -36,6 +38,7 @@ export default (): AppConfig => {
redmineStatusChanges: redmineStatusChanges, redmineStatusChanges: redmineStatusChanges,
redmineEccm: redmineEccm, redmineEccm: redmineEccm,
redmineCurrentUserRules: redmineCurrentUserRules, redmineCurrentUserRules: redmineCurrentUserRules,
simpleKanbanBoard: simpleKanbanBoard,
}; };
return appConfig; return appConfig;

View file

@ -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;
};

View file

@ -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<any> {
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',
],
});
}
}

View file

@ -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<string>('simpleKanbanBoard.path');
}
@Get('/tree/:name/raw')
async getRawData(@Param('name') name: string): Promise<any> {
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<any> {
return await this.getRawData(name);
}
}

View file

@ -1,4 +1,5 @@
import { MainConfigModel } from '@app/event-emitter/models/main-config-model'; 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 { EccmConfig } from './eccm-config.model';
import { StatusChangesConfig } from './status-changes-config.model'; import { StatusChangesConfig } from './status-changes-config.model';
import { StatusesConfig } from './statuses-config.model'; import { StatusesConfig } from './statuses-config.model';
@ -9,6 +10,7 @@ export type AppConfig = {
redmineStatusChanges: StatusChangesConfig.Config; redmineStatusChanges: StatusChangesConfig.Config;
redmineEccm: EccmConfig.Config; redmineEccm: EccmConfig.Config;
redmineCurrentUserRules: Record<string, string>; redmineCurrentUserRules: Record<string, string>;
simpleKanbanBoard: SimpleKanbanBoardConfig;
couchDb: { couchDb: {
dbs: { dbs: {
changes: string; changes: string;

View file

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECCM 1.10 Kanban board</title>
<style>
.kanban-container {
display: flex;
flex-direction: row;
width: 100%;
}
.kanban-column {
width: 200px;
background-color: rgb(225, 225, 225);
display: flex;
flex-direction: column;
}
.kanban-header {
text-align: center;
width: 200px;
}
.kanban-card {
background-color: rgb(196, 196, 196);
border-width: 1px;
border-color: rgba(255, 255, 255, 0.2) rgba(96, 96, 96, 0.2) rgba(96, 96, 96, 0.2) rgba(255, 255, 255, 0.2);
/*border-color: black;*/
border-style: solid;
margin: 2px;
padding: 3px;
width: 190px;
/*display: flex;*/
box-shadow: 0 0 3px rgba(0,0,0,0.5);
border-radius: 3px;
z-index: 100;
}
.kanban-card div {
font-size: small;
}
.kanban-card .kanban-card-title {
font-weight: bold;
}
</style>
</head>
<body>
{{#each this}}
<h1>{{this.metainfo.title}}</h1>
<div class="kanban-container">
{{#each this.data}}
<div class="kanban-column">
<div class="kanban-header">{{this.status}}</div>
{{#each this.issues}}
<div class="kanban-card">
<div class="kanban-card-title">{{this.tracker.name}} #{{this.id}} - {{this.subject}}</div>
<div>Исп.: {{this.current_user.name}}</div>
<div>Прогресс: {{this.done_ration}}</div>
<div>Трудозатраты: {{this.total_spent_hours}} / {{this.total_estimated_hours}}</div>
</div>
{{/each}}
</div>
{{/each}}
</div>
{{/each}}
</body>
</html>