Рефакторинг - разделены провайдеры данных от виджетов с представлениями

This commit is contained in:
Pavel Gnedov 2023-10-02 19:15:05 +07:00
parent c8e71d3926
commit 10b04bdb0f
21 changed files with 431 additions and 270 deletions

View file

@ -1,36 +1,45 @@
import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
import { DashboardsService } from './dashboards.service'; import { DashboardsService } from './dashboards.service';
import { BadRequestErrorHandler, getOrAppErrorOrThrow } from '../utils/result'; import { BadRequestErrorHandler, getOrAppErrorOrThrow } from '../utils/result';
import { DashboardsDataService } from './dashboards-data.service';
@Controller('dashboard') @Controller('dashboard')
export class DashboardController { export class DashboardController {
constructor(private dashboardsService: DashboardsService) {} constructor(
private dashboardsService: DashboardsService,
private dashboardsDataService: DashboardsDataService,
) {}
@Post(':name') @Post()
async create(@Param('name') name: string): Promise<string> { async create(): Promise<string> {
if (name === 'anonymous') {
name = null;
}
const res = await getOrAppErrorOrThrow( const res = await getOrAppErrorOrThrow(
() => this.dashboardsService.create(name), () => this.dashboardsService.create(),
BadRequestErrorHandler, BadRequestErrorHandler,
); );
return res.id; return res.id;
} }
@Get(':name') @Get(':id')
async load(@Param('name') name: string): Promise<any> { async load(@Param('id') id: string): Promise<any> {
const res = await getOrAppErrorOrThrow( const res = await getOrAppErrorOrThrow(
() => this.dashboardsService.load(name), () => this.dashboardsService.load(id),
BadRequestErrorHandler, BadRequestErrorHandler,
); );
return res; return res;
} }
@Put(':name') @Get(':id/load-data')
async save(@Param('name') name: string, @Body() data: any): Promise<void> { async loadData(@Param('id') id: string): Promise<any> {
return await getOrAppErrorOrThrow(
() => this.dashboardsDataService.loadData(id),
BadRequestErrorHandler,
);
}
@Put(':id')
async save(@Param('id') id: string, @Body() data: any): Promise<void> {
const res = await getOrAppErrorOrThrow( const res = await getOrAppErrorOrThrow(
() => this.dashboardsService.save(name, data), () => this.dashboardsService.save(id, data),
BadRequestErrorHandler, BadRequestErrorHandler,
); );
return res; return res;

View file

@ -0,0 +1,67 @@
import { Injectable, Logger } from '@nestjs/common';
import { DashboardsService } from './dashboards.service';
import * as DashboardModel from '../models/dashboard';
import {
AppError,
Result,
createAppError,
fail,
success,
} from '../utils/result';
import { WidgetsCollectionService } from './widgets-collection.service';
export type WidgetWithData = {
widget: DashboardModel.Widget;
data: any;
};
@Injectable()
export class DashboardsDataService {
private logger = new Logger(DashboardsDataService.name);
constructor(
private dashboardsService: DashboardsService,
private widgetsCollectionService: WidgetsCollectionService,
) {}
async loadData(id: string): Promise<WidgetWithData[]> {
const cfg = await this.dashboardsService.load(id);
const results: WidgetWithData[] = [];
let isSuccess = false;
for (let i = 0; i < cfg.widgets.length; i++) {
const widget = cfg.widgets[i];
const loadRes = await this.loadWidgetData(
widget.type,
widget.widgetParams,
widget.dataLoaderParams,
cfg,
);
if (loadRes.result) {
isSuccess = true;
results.push({
widget: widget,
data: loadRes.result,
});
}
}
if (!isSuccess) throw createAppError('CANNOT_LOAD_DATA');
return results;
}
async loadWidgetData(
type: string,
widgetParams: DashboardModel.WidgetParams,
dataLoaderParams: DashboardModel.DataLoaderParams,
dashboardParams: DashboardModel.Data,
): Promise<Result<any, AppError>> {
const widgetResult = this.widgetsCollectionService.getWidgetByType(type);
if (widgetResult.error) return fail(createAppError(widgetResult.error));
const widget = widgetResult.result;
const renderResult = await widget.render(
widgetParams,
dataLoaderParams,
dashboardParams,
);
return renderResult;
}
}

View file

@ -1,9 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Dashboards as DashboardsDb } from '../couchdb-datasources/dashboards'; import { Dashboards as DashboardsDb } from '../couchdb-datasources/dashboards';
import { import * as DashboardModel from '../models/dashboard';
Dashboard as DashboardModel,
Data as DashboardData,
} from '../models/dashboard';
import nano from 'nano'; import nano from 'nano';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { createAppError } from '../utils/result'; import { createAppError } from '../utils/result';
@ -14,58 +11,56 @@ export class DashboardsService {
constructor(private db: DashboardsDb) {} constructor(private db: DashboardsDb) {}
async create(name?: string): Promise<DashboardModel> { async create(): Promise<DashboardModel.Dashboard> {
if (!name) { const id = randomUUID();
name = randomUUID(); this.logger.debug(`Create new dashboard with id - ${id}`);
} if (await this.isExists(id)) {
this.logger.debug(`Create new dashboard with name - ${name}`);
if (await this.isExists(name)) {
const err = createAppError('ALREADY_EXISTS'); const err = createAppError('ALREADY_EXISTS');
this.logger.error(`Error - ${JSON.stringify(err)}`); this.logger.error(`Error - ${JSON.stringify(err)}`);
throw err; throw err;
} }
const ds = await this.db.getDatasource(); const ds = await this.db.getDatasource();
const doc: nano.MaybeDocument & DashboardModel = { const doc: nano.MaybeDocument & DashboardModel.Dashboard = {
_id: name, _id: id,
id: name, id: id,
data: null, data: null,
}; };
await ds.insert(doc); await ds.insert(doc);
return await ds.get(name); return await ds.get(id);
} }
async loadRawData( async loadRawData(
name: string, id: string,
): Promise<DashboardModel & nano.MaybeDocument> { ): Promise<DashboardModel.Dashboard & nano.MaybeDocument> {
this.logger.debug(`Load raw data, dashboard name - ${name}`); this.logger.debug(`Load raw data, dashboard id - ${id}`);
const ds = await this.db.getDatasource(); const ds = await this.db.getDatasource();
if (!(await this.isExists(name))) throw createAppError('NOT_EXISTS'); if (!(await this.isExists(id))) throw createAppError('NOT_EXISTS');
const res = await ds.get(name); const res = await ds.get(id);
return res; return res;
} }
async load(name: string): Promise<DashboardData> { async load(id: string): Promise<DashboardModel.Data> {
this.logger.debug(`Load dashboard name - ${name}`); this.logger.debug(`Load dashboard id - ${id}`);
const rawData = await this.loadRawData(name); const rawData = await this.loadRawData(id);
return rawData.data; return rawData.data;
} }
async isExists(name: string): Promise<boolean> { async isExists(id: string): Promise<boolean> {
const ds = await this.db.getDatasource(); const ds = await this.db.getDatasource();
try { try {
await ds.get(name); await ds.get(id);
return true; return true;
} catch (ex) { } catch (ex) {
return false; return false;
} }
} }
async save(name: string, data: any): Promise<void> { async save(id: string, data: DashboardModel.Data): Promise<void> {
this.logger.debug( this.logger.debug(
`Save dashboard name - ${name}, data - ${JSON.stringify(data)}`, `Save dashboard id - ${id}, data - ${JSON.stringify(data)}`,
); );
const ds = await this.db.getDatasource(); const ds = await this.db.getDatasource();
const prevValue = await this.loadRawData(name); const prevValue = await this.loadRawData(id);
const newValue = { const newValue = {
_id: prevValue._id, _id: prevValue._id,

View file

@ -0,0 +1,28 @@
import { Result, AppError } from '../utils/result';
import { WidgetDataLoaderInterface } from './widget-data-loader-interface';
import { WidgetInterface } from './widget-interface';
export class InteractiveWidget
implements WidgetInterface<any, any, any, any, any>
{
constructor(
public dataLoader: WidgetDataLoaderInterface<any, any, any>,
public type: string,
) {}
async render(
widgetParams: any,
dataLoaderParams: any,
dashboardParams: any,
): Promise<Result<any, AppError>> {
const data = await this.dataLoader.load(dataLoaderParams, dashboardParams);
return data;
}
}
export function createInteractiveWidget(
dataLoader: WidgetDataLoaderInterface<any, any, any>,
type: string,
): WidgetInterface<any, any, any, any, any> {
return new InteractiveWidget(dataLoader, type);
}

View file

@ -0,0 +1,35 @@
import { Result, AppError, success } from '../utils/result';
import { WidgetDataLoaderInterface } from './widget-data-loader-interface';
import { WidgetInterface } from './widget-interface';
import Handlebars from 'handlebars';
export class TextWidget implements WidgetInterface<any, any, any, any, any> {
constructor(
public dataLoader: WidgetDataLoaderInterface<any, any, any>,
public type: string,
public template: string,
) {}
async render(
widgetParams: any,
dataLoaderParams: any,
dashboardParams: any,
): Promise<Result<any, AppError>> {
const params = {
widgetParams,
dataLoaderParams,
dashboardParams,
};
const template = Handlebars.compile(this.template);
const res = template(params);
return success(res);
}
}
export function createTextWidget(
dataLoader: WidgetDataLoaderInterface<any, any, any>,
type: string,
template: string,
): WidgetInterface<any, any, any, any, any> {
return new TextWidget(dataLoader, type, template);
}

View file

@ -0,0 +1,9 @@
import { AppError, Result } from '../utils/result';
export interface WidgetDataLoaderInterface<DLP, DBP, R> {
isMyConfig(dataLoaderParams: DLP): boolean;
load(
dataLoaderParams: DLP,
dashboardParams: DBP,
): Promise<Result<R, AppError>>;
}

View file

@ -12,8 +12,14 @@ import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store'; import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import nano from 'nano'; import nano from 'nano';
import { WidgetInterface } from '../widget-interface';
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer'; import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
import { WidgetDataLoaderInterface } from '../widget-data-loader-interface';
import {
AppError,
Result,
createAppError,
success,
} from '@app/event-emitter/utils/result';
export namespace ListIssuesByFieldsWidgetNs { export namespace ListIssuesByFieldsWidgetNs {
export type Params = { export type Params = {
@ -34,10 +40,10 @@ export namespace ListIssuesByFieldsWidgetNs {
type Params = ListIssuesByFieldsWidgetNs.Params; type Params = ListIssuesByFieldsWidgetNs.Params;
@Injectable() @Injectable()
export class ListIssuesByFieldsWidgetService export class ListIssuesByFieldsWidgetDataLoaderService
implements WidgetInterface<Params, any, any> implements WidgetDataLoaderInterface<Params, any, any>
{ {
private logger = new Logger(ListIssuesByFieldsWidgetService.name); private logger = new Logger(ListIssuesByFieldsWidgetDataLoaderService.name);
private issuesLoader: IssuesServiceNs.IssuesLoader; private issuesLoader: IssuesServiceNs.IssuesLoader;
constructor( constructor(
@ -52,7 +58,7 @@ export class ListIssuesByFieldsWidgetService
return true; return true;
} }
async render(widgetParams: Params): Promise<any> { async load(widgetParams: Params): Promise<Result<any, AppError>> {
let store: FlatIssuesStore; let store: FlatIssuesStore;
if (widgetParams.fromRootIssueId) { if (widgetParams.fromRootIssueId) {
store = await this.getListFromRoot(widgetParams.fromRootIssueId); store = await this.getListFromRoot(widgetParams.fromRootIssueId);
@ -61,7 +67,7 @@ export class ListIssuesByFieldsWidgetService
} else { } else {
const errMsg = `Wrong widgetParams value`; const errMsg = `Wrong widgetParams value`;
this.logger.error(errMsg); this.logger.error(errMsg);
throw new Error(errMsg); return fail(createAppError(errMsg));
} }
await store.enhanceIssues([ await store.enhanceIssues([
this.timePassedHighlightEnhancer, this.timePassedHighlightEnhancer,
@ -87,7 +93,7 @@ export class ListIssuesByFieldsWidgetService
return a.metainfo.title.localeCompare(b.metainfo.title); return a.metainfo.title.localeCompare(b.metainfo.title);
}); });
} }
return res; return success(res);
} }
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> { private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {

View file

@ -11,8 +11,14 @@ import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store'; import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import nano from 'nano'; import nano from 'nano';
import { WidgetInterface } from '../widget-interface';
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer'; import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
import { WidgetDataLoaderInterface } from '../widget-data-loader-interface';
import {
AppError,
Result,
createAppError,
success,
} from '@app/event-emitter/utils/result';
export namespace ListIssuesByUsersLikeJiraWidgetNs { export namespace ListIssuesByUsersLikeJiraWidgetNs {
export namespace Models { export namespace Models {
@ -29,15 +35,17 @@ export namespace ListIssuesByUsersLikeJiraWidgetNs {
type Params = ListIssuesByUsersLikeJiraWidgetNs.Models.Params; type Params = ListIssuesByUsersLikeJiraWidgetNs.Models.Params;
@Injectable() @Injectable()
export class ListIssuesByUsersLikeJiraWidgetService export class ListIssuesByUsersLikeJiraWidgetDataLoaderService
implements WidgetInterface<Params, any, any> implements WidgetDataLoaderInterface<Params, any, any>
{ {
private logger = new Logger(ListIssuesByUsersLikeJiraWidgetService.name); private logger = new Logger(
ListIssuesByUsersLikeJiraWidgetDataLoaderService.name,
);
private issuesLoader: IssuesServiceNs.IssuesLoader; private issuesLoader: IssuesServiceNs.IssuesLoader;
constructor( constructor(
private issuesService: IssuesService,
private timePassedHighlightEnhancer: TimePassedHighlightEnhancer, private timePassedHighlightEnhancer: TimePassedHighlightEnhancer,
private issuesService: IssuesService,
private issueUrlEnhancer: IssueUrlEnhancer, private issueUrlEnhancer: IssueUrlEnhancer,
) { ) {
this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); this.issuesLoader = this.issuesService.createDynamicIssuesLoader();
@ -47,7 +55,7 @@ export class ListIssuesByUsersLikeJiraWidgetService
return true; return true;
} }
async render(widgetParams: Params): Promise<any> { async load(widgetParams: Params): Promise<Result<any, AppError>> {
let store: FlatIssuesStore; let store: FlatIssuesStore;
if (widgetParams.fromRootIssueId) { if (widgetParams.fromRootIssueId) {
store = await this.getListFromRoot(widgetParams.fromRootIssueId); store = await this.getListFromRoot(widgetParams.fromRootIssueId);
@ -56,7 +64,7 @@ export class ListIssuesByUsersLikeJiraWidgetService
} else { } else {
const errMsg = `Wrong widgetParams value`; const errMsg = `Wrong widgetParams value`;
this.logger.error(errMsg); this.logger.error(errMsg);
throw new Error(errMsg); return fail(createAppError(errMsg));
} }
await store.enhanceIssues([ await store.enhanceIssues([
this.timePassedHighlightEnhancer, this.timePassedHighlightEnhancer,
@ -92,7 +100,7 @@ export class ListIssuesByUsersLikeJiraWidgetService
return a.metainfo.title.localeCompare(b.metainfo.title); return a.metainfo.title.localeCompare(b.metainfo.title);
}); });
} }
return res; return success(res);
} }
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> { private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {

View file

@ -12,8 +12,14 @@ import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store'; import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import nano from 'nano'; import nano from 'nano';
import { WidgetInterface } from '../widget-interface';
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer'; import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
import { WidgetDataLoaderInterface } from '../widget-data-loader-interface';
import {
AppError,
Result,
createAppError,
success,
} from '@app/event-emitter/utils/result';
export namespace ListIssuesByUsersWidgetNs { export namespace ListIssuesByUsersWidgetNs {
export namespace Models { export namespace Models {
@ -41,10 +47,10 @@ type ExtendedIssue = RedmineTypes.Issue & Record<string, any>;
type FindResult = ListIssuesByUsersWidgetNs.Models.FindResult; type FindResult = ListIssuesByUsersWidgetNs.Models.FindResult;
@Injectable() @Injectable()
export class ListIssuesByUsersWidgetService export class ListIssuesByUsersWidgetDataLoaderService
implements WidgetInterface<Params, any, any> implements WidgetDataLoaderInterface<Params, any, any>
{ {
private logger = new Logger(ListIssuesByUsersWidgetService.name); private logger = new Logger(ListIssuesByUsersWidgetDataLoaderService.name);
private issuesLoader: IssuesServiceNs.IssuesLoader; private issuesLoader: IssuesServiceNs.IssuesLoader;
constructor( constructor(
@ -59,7 +65,7 @@ export class ListIssuesByUsersWidgetService
return true; return true;
} }
async render(widgetParams: Params): Promise<any> { async load(widgetParams: Params): Promise<Result<any, AppError>> {
let store: FlatIssuesStore; let store: FlatIssuesStore;
if (widgetParams.fromRootIssueId) { if (widgetParams.fromRootIssueId) {
store = await this.getListFromRoot(widgetParams.fromRootIssueId); store = await this.getListFromRoot(widgetParams.fromRootIssueId);
@ -68,7 +74,7 @@ export class ListIssuesByUsersWidgetService
} else { } else {
const errMsg = `Wrong widgetParams value`; const errMsg = `Wrong widgetParams value`;
this.logger.error(errMsg); this.logger.error(errMsg);
throw new Error(errMsg); return fail(createAppError(errMsg));
} }
await store.enhanceIssues([ await store.enhanceIssues([
this.timePassedHighlightEnhancer, this.timePassedHighlightEnhancer,
@ -95,7 +101,7 @@ export class ListIssuesByUsersWidgetService
return a.metainfo.title.localeCompare(b.metainfo.title); return a.metainfo.title.localeCompare(b.metainfo.title);
}); });
} }
return res; return success(res);
} }
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> { private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {

View file

@ -11,8 +11,9 @@ import {
TreeIssuesStoreNs, TreeIssuesStoreNs,
} from '@app/event-emitter/utils/tree-issues-store'; } from '@app/event-emitter/utils/tree-issues-store';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WidgetInterface } from '../widget-interface';
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer'; import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
import { WidgetDataLoaderInterface } from '../widget-data-loader-interface';
import { AppError, Result, success } from '@app/event-emitter/utils/result';
export namespace RootIssueSubTreesWidgetNs { export namespace RootIssueSubTreesWidgetNs {
export namespace Models { export namespace Models {
@ -39,8 +40,8 @@ export namespace RootIssueSubTreesWidgetNs {
type Params = RootIssueSubTreesWidgetNs.Models.Params; type Params = RootIssueSubTreesWidgetNs.Models.Params;
@Injectable() @Injectable()
export class RootIssueSubTreesWidgetService export class RootIssueSubTreesWidgetDataLoaderService
implements WidgetInterface<Params, any, any> implements WidgetDataLoaderInterface<Params, any, any>
{ {
private issuesLoader: IssuesServiceNs.IssuesLoader; private issuesLoader: IssuesServiceNs.IssuesLoader;
@ -56,7 +57,7 @@ export class RootIssueSubTreesWidgetService
return true; return true;
} }
async render(widgetParams: Params): Promise<any> { async load(widgetParams: Params): Promise<Result<any, AppError>> {
const treeStore = new TreeIssuesStore(); const treeStore = new TreeIssuesStore();
const rootIssue = await this.issuesService.getIssue( const rootIssue = await this.issuesService.getIssue(
widgetParams.rootIssueId, widgetParams.rootIssueId,
@ -89,11 +90,12 @@ export class RootIssueSubTreesWidgetService
} }
} }
} }
return stories.map((s) => { const res = stories.map((s) => {
return { return {
data: s.store.groupByStatus(widgetParams.statuses), data: s.store.groupByStatus(widgetParams.statuses),
metainfo: s.metainfo, metainfo: s.metainfo,
}; };
}); });
return success(res);
} }
} }

View file

@ -0,0 +1,19 @@
import { AppError, Result } from '../utils/result';
import { WidgetDataLoaderInterface } from './widget-data-loader-interface';
/**
* - WP - widget params
* - DLP - dataloader params
* - DBP - dashboard params
* - DLR - dataloader result
* - R - result
*/
export interface WidgetInterface<WP, DLP, DBP, DLR, R> {
dataLoader: WidgetDataLoaderInterface<DLP, DBP, DLR>;
type: string;
render(
widgetParams: WP,
dataLoaderParams: DLP,
dashboardParams: DBP,
): Promise<Result<R, AppError>>;
}

View file

@ -0,0 +1,68 @@
import { Injectable } from '@nestjs/common';
import { WidgetInterface } from './widget-interface';
import { ListIssuesByFieldsWidgetDataLoaderService } from './widget-data-loader/list-issues-by-fields.widget-data-loader.service';
import { ListIssuesByUsersLikeJiraWidgetDataLoaderService } from './widget-data-loader/list-issues-by-users-like-jira.widget-data-loader.service';
import { RootIssueSubTreesWidgetDataLoaderService } from './widget-data-loader/root-issue-subtrees.widget-data-loader.service';
import { createInteractiveWidget } from './interactive-widget-factory';
import { Result, success } from '@app/event-emitter/utils/result';
@Injectable()
export class WidgetsCollectionService {
collection: WidgetInterface<any, any, any, any, any>[] = [];
constructor(
private listIssuesByFieldsWidgetDataLoaderService: ListIssuesByFieldsWidgetDataLoaderService,
private listIssuesByUsersLikeJiraWidgetDataLoaderService: ListIssuesByUsersLikeJiraWidgetDataLoaderService,
private rootIssueSubTreesWidgetDataLoaderService: RootIssueSubTreesWidgetDataLoaderService,
) {
const collection = [
createInteractiveWidget(
this.listIssuesByFieldsWidgetDataLoaderService,
'kanban_by_fields',
),
createInteractiveWidget(
this.listIssuesByUsersLikeJiraWidgetDataLoaderService,
'kanban_by_users',
),
createInteractiveWidget(
this.rootIssueSubTreesWidgetDataLoaderService,
'kanban_by_tree',
),
createInteractiveWidget(
this.listIssuesByFieldsWidgetDataLoaderService,
'issues_list_by_fields',
),
createInteractiveWidget(
this.listIssuesByUsersLikeJiraWidgetDataLoaderService,
'issues_list_by_users',
),
createInteractiveWidget(
this.rootIssueSubTreesWidgetDataLoaderService,
'issues_list_by_tree',
),
];
collection.forEach((w) => this.appendWidget(w));
}
appendWidget(
widget: WidgetInterface<any, any, any, any, any>,
): Result<true, string> {
const type = widget.type;
const isExists = this.collection.find((w) => w.type === type);
if (isExists) return fail('WIDGET_WITH_SAME_TYPE_ALREADY_EXISTS');
this.collection.push(widget);
return success(true);
}
getWidgetTypes(): string[] {
return this.collection.map((w) => w.type);
}
getWidgetByType(
type: string,
): Result<WidgetInterface<any, any, any, any, any>, string> {
const widget = this.collection.find((w) => w.type === type);
return widget ? success(widget) : fail('WIDGET_WITH_SAME_TYPE_NOT_FOUND');
}
}

View file

@ -18,15 +18,10 @@ import { IssuesService } from './issues/issues.service';
import { IssuesController } from './issues/issues.controller'; import { IssuesController } from './issues/issues.controller';
import { TimestampEnhancer } from './issue-enhancers/timestamps-enhancer'; 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 { RootIssueSubTreesWidgetService } from './project-dashboard/widgets/root-issue-subtrees.widget.service';
import { DynamicLoader } from './configs/dynamic-loader'; import { DynamicLoader } from './configs/dynamic-loader';
import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter'; import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter';
import { IssueUrlEnhancer } from './issue-enhancers/issue-url-enhancer'; import { IssueUrlEnhancer } from './issue-enhancers/issue-url-enhancer';
import { ListIssuesByUsersWidgetService } from './project-dashboard/widgets/list-issues-by-users.widget.service';
import { ListIssuesByUsersLikeJiraWidgetService } from './project-dashboard/widgets/list-issues-by-users-like-jira.widget.service';
import { TimePassedHighlightEnhancer } from './issue-enhancers/time-passed-highlight-enhancer'; import { TimePassedHighlightEnhancer } from './issue-enhancers/time-passed-highlight-enhancer';
import { ListIssuesByFieldsWidgetService } from './project-dashboard/widgets/list-issues-by-fields.widget.service';
import { IssuesUpdaterService } from './issues-updater/issues-updater.service'; import { IssuesUpdaterService } from './issues-updater/issues-updater.service';
import { CalendarEnhancer } from './issue-enhancers/calendar-enhancer'; import { CalendarEnhancer } from './issue-enhancers/calendar-enhancer';
import { CalendarService } from './calendar/calendar.service'; import { CalendarService } from './calendar/calendar.service';
@ -34,6 +29,12 @@ import { CalendarController } from './calendar/calendar.controller';
import { Dashboards as DashboardsDs } from './couchdb-datasources/dashboards'; import { Dashboards as DashboardsDs } from './couchdb-datasources/dashboards';
import { DashboardController } from './dashboards/dashboard.controller'; import { DashboardController } from './dashboards/dashboard.controller';
import { DashboardsService } from './dashboards/dashboards.service'; import { DashboardsService } from './dashboards/dashboards.service';
import { DashboardsDataService } from './dashboards/dashboards-data.service';
import { RootIssueSubTreesWidgetDataLoaderService } from './dashboards/widget-data-loader/root-issue-subtrees.widget-data-loader.service';
import { ListIssuesByUsersWidgetDataLoaderService } from './dashboards/widget-data-loader/list-issues-by-users.widget-data-loader.service';
import { ListIssuesByUsersLikeJiraWidgetDataLoaderService } from './dashboards/widget-data-loader/list-issues-by-users-like-jira.widget-data-loader.service';
import { ListIssuesByFieldsWidgetDataLoaderService } from './dashboards/widget-data-loader/list-issues-by-fields.widget-data-loader.service';
import { WidgetsCollectionService } from './dashboards/widgets-collection.service';
@Module({}) @Module({})
export class EventEmitterModule implements OnModuleInit { export class EventEmitterModule implements OnModuleInit {
@ -57,15 +58,14 @@ export class EventEmitterModule implements OnModuleInit {
IssuesService, IssuesService,
TimestampEnhancer, TimestampEnhancer,
EnhancerService, EnhancerService,
ProjectDashboardService, RootIssueSubTreesWidgetDataLoaderService,
RootIssueSubTreesWidgetService,
DynamicLoader, DynamicLoader,
RedminePublicUrlConverter, RedminePublicUrlConverter,
IssueUrlEnhancer, IssueUrlEnhancer,
ListIssuesByUsersWidgetService, ListIssuesByUsersWidgetDataLoaderService,
ListIssuesByUsersLikeJiraWidgetService, ListIssuesByUsersLikeJiraWidgetDataLoaderService,
TimePassedHighlightEnhancer, TimePassedHighlightEnhancer,
ListIssuesByFieldsWidgetService, ListIssuesByFieldsWidgetDataLoaderService,
{ {
provide: 'ISSUES_UPDATER_SERVICE', provide: 'ISSUES_UPDATER_SERVICE',
useFactory: (configService: ConfigService) => { useFactory: (configService: ConfigService) => {
@ -101,6 +101,8 @@ export class EventEmitterModule implements OnModuleInit {
inject: ['CALENDAR_ENHANCER', IssuesService], inject: ['CALENDAR_ENHANCER', IssuesService],
}, },
DashboardsService, DashboardsService,
DashboardsDataService,
WidgetsCollectionService,
], ],
exports: [ exports: [
EventEmitterService, EventEmitterService,
@ -116,15 +118,14 @@ export class EventEmitterModule implements OnModuleInit {
IssuesService, IssuesService,
TimestampEnhancer, TimestampEnhancer,
EnhancerService, EnhancerService,
ProjectDashboardService, RootIssueSubTreesWidgetDataLoaderService,
RootIssueSubTreesWidgetService,
DynamicLoader, DynamicLoader,
RedminePublicUrlConverter, RedminePublicUrlConverter,
IssueUrlEnhancer, IssueUrlEnhancer,
ListIssuesByUsersWidgetService, ListIssuesByUsersWidgetDataLoaderService,
ListIssuesByUsersLikeJiraWidgetService, ListIssuesByUsersLikeJiraWidgetDataLoaderService,
TimePassedHighlightEnhancer, TimePassedHighlightEnhancer,
ListIssuesByFieldsWidgetService, ListIssuesByFieldsWidgetDataLoaderService,
{ {
provide: 'ISSUES_UPDATER_SERVICE', provide: 'ISSUES_UPDATER_SERVICE',
useExisting: 'ISSUES_UPDATER_SERVICE', useExisting: 'ISSUES_UPDATER_SERVICE',
@ -138,6 +139,8 @@ export class EventEmitterModule implements OnModuleInit {
useExisting: 'CALENDAR_SERVICE', useExisting: 'CALENDAR_SERVICE',
}, },
DashboardsService, DashboardsService,
DashboardsDataService,
WidgetsCollectionService,
], ],
controllers: [ controllers: [
MainController, MainController,

View file

@ -1,6 +1,29 @@
export type Data = Record<string, any> | null; export type Data = {
widgets: Widget[];
title: string;
} | null;
export type Dashboard = { export type Dashboard = {
id: string; id: string;
data: Data; data: Data;
}; };
/**
* Параметры для отрисовки данных
*/
export type WidgetParams = {
collapsed?: boolean;
} & Record<string, any>;
/**
* Параметры для загрузки данных
*/
export type DataLoaderParams = Record<string, any> | null;
export type Widget = {
type: string;
id: string;
title: string;
widgetParams?: WidgetParams;
dataLoaderParams?: DataLoaderParams;
};

View file

@ -1,152 +0,0 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { Injectable } from '@nestjs/common';
import { RedmineTypes } from '../models/redmine-types';
export namespace ProjectDashboard {
export namespace Models {
export type Params = {
projectName: string;
workers: Worker[];
filter: FilterDefination[];
statuses: Status[];
};
export type Worker = {
id?: number;
firstname?: string;
lastname?: string;
name?: string;
};
export enum FilterTypes {
TREE = 'TREE',
LIST = 'LIST',
DYNAMIC_LIST = 'DYNAMIC_LIST',
VERSION = 'VERSION',
}
export namespace FilterParams {
export type Tree = {
rootIssueId: number;
subtrees: SubTree[];
showOthers: boolean;
};
export type SubTree = {
title: string;
issueId: number;
};
export type List = {
issueIds: number[];
};
export type Version = {
version: string;
};
export type DynamicList = {
selector: Record<string, any>;
};
export type AnyFilterParams = Tree | List | DynamicList | Version;
}
export type FilterParams = Record<string, any>;
export namespace FilterResults {
export type Tree = List;
export type List = { status: string; issues: RedmineTypes.Issue[] }[];
export type DynamicList = List;
export type Version = List;
export type AnyFilterResults = List | Tree | DynamicList | Version;
}
export type FilterResult = Record<string, any>[];
export type AnalyticFunction = {
functionName: string;
};
export type FilterDefination = {
type: FilterTypes;
title: string;
params: FilterParams.AnyFilterParams;
};
export type FilterWithResults = {
params: FilterDefination;
results: FilterResults.AnyFilterResults;
};
export type Status = {
name: string;
closed: boolean;
};
}
export function CheckWorker(worker: Models.Worker): boolean {
return Boolean(
(typeof worker.id === 'number' && worker.id >= 0) ||
(worker.firstname && worker.lastname) ||
worker.name,
);
}
export class SingleProject {
// TODO: code for SingleProject
constructor(private params: Models.Params) {
return;
}
}
export namespace Widgets {
// Чё будет делать виджет?
// * рендер - из параметров будет создавать данные с какими-либо расчётами
export interface Widget {
render(
filterParams: Models.FilterParams,
dashboardParams: Models.Params,
): Models.FilterResult;
}
export class List implements Widget {
render(
filterParams: Models.FilterParams,
dashboardParams: Models.Params,
): Models.FilterResult {
throw new Error('Method not implemented.');
}
}
export class DynamicList implements Widget {
render(
filterParams: Models.FilterParams,
dashboardParams: Models.Params,
): Models.FilterResult {
throw new Error('Method not implemented.');
}
}
export class Tree implements Widget {
render(
filterParams: Models.FilterParams,
dashboardParams: Models.Params,
): Models.FilterResult {
throw new Error('Method not implemented.');
}
}
export class Version implements Widget {
render(
filterParams: Models.FilterParams,
dashboardParams: Models.Params,
): Models.FilterResult {
throw new Error('Method not implemented.');
}
}
}
}
@Injectable()
export class ProjectDashboardService {
constructor() {
return;
}
}

View file

@ -1,4 +0,0 @@
export interface WidgetInterface<W, D, R> {
isMyConfig(widgetParams: W): boolean;
render(widgetParams: W, dashboardParams: D): Promise<R>;
}

View file

@ -43,7 +43,7 @@ import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handler
import { DailyEccmWithExtraDataService } from './reports/daily-eccm-with-extra-data.service'; import { DailyEccmWithExtraDataService } from './reports/daily-eccm-with-extra-data.service';
import { SimpleKanbanBoardController } from './dashboards/simple-kanban-board.controller'; import { SimpleKanbanBoardController } from './dashboards/simple-kanban-board.controller';
import { IssueUrlEnhancer } from '@app/event-emitter/issue-enhancers/issue-url-enhancer'; import { IssueUrlEnhancer } from '@app/event-emitter/issue-enhancers/issue-url-enhancer';
import { IssuesByTagsWidgetService } from './dashboards/widgets/issues-by-tags.widget.service'; import { IssuesByTagsWidgetDataLoaderService } from './dashboards/widget-data-loader/issues-by-tags.widget-data-loader.service';
import { CategoryMergeToTagsEnhancer } from './issue-enhancers/category-merge-to-tags-enhancer'; import { CategoryMergeToTagsEnhancer } from './issue-enhancers/category-merge-to-tags-enhancer';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path'; import { join } from 'path';
@ -52,6 +52,7 @@ import { TagsManagerController } from './tags-manager/tags-manager.controller';
import { CreateTagManagerServiceProvider } from './tags-manager/tags-manager.service'; import { CreateTagManagerServiceProvider } from './tags-manager/tags-manager.service';
import { CalendarEnhancer } from '@app/event-emitter/issue-enhancers/calendar-enhancer'; import { CalendarEnhancer } from '@app/event-emitter/issue-enhancers/calendar-enhancer';
import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasources/dashboards'; import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasources/dashboards';
import { DashboardInitService } from './dashboards/dashboard-init.service';
@Module({ @Module({
imports: [ imports: [
@ -99,7 +100,7 @@ import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasourc
DailyEccmUserCommentsService, DailyEccmUserCommentsService,
SetDailyEccmUserCommentBotHandlerService, SetDailyEccmUserCommentBotHandlerService,
DailyEccmWithExtraDataService, DailyEccmWithExtraDataService,
IssuesByTagsWidgetService, IssuesByTagsWidgetDataLoaderService,
{ {
provide: 'CATEGORY_MERGE_TO_TAGS_ENHANCER', provide: 'CATEGORY_MERGE_TO_TAGS_ENHANCER',
useFactory: (configService: ConfigService) => { useFactory: (configService: ConfigService) => {
@ -111,6 +112,7 @@ import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasourc
inject: [ConfigService], inject: [ConfigService],
}, },
CreateTagManagerServiceProvider('TAG_MANAGER_SERVICE'), CreateTagManagerServiceProvider('TAG_MANAGER_SERVICE'),
DashboardInitService,
], ],
}) })
export class AppModule implements OnModuleInit { export class AppModule implements OnModuleInit {
@ -137,6 +139,8 @@ export class AppModule implements OnModuleInit {
@Inject('CALENDAR_ENHANCER') @Inject('CALENDAR_ENHANCER')
private calendarEnhancer: CalendarEnhancer, private calendarEnhancer: CalendarEnhancer,
private dashboardInitService: DashboardInitService,
) {} ) {}
onModuleInit() { onModuleInit() {
@ -215,6 +219,7 @@ export class AppModule implements OnModuleInit {
}); });
this.initDailyEccmUserCommentsPipeline(); this.initDailyEccmUserCommentsPipeline();
this.initDashbordProviders();
} }
private initDailyEccmUserCommentsPipeline(): void { private initDailyEccmUserCommentsPipeline(): void {
@ -228,4 +233,8 @@ export class AppModule implements OnModuleInit {
}, },
); );
} }
private initDashbordProviders(): void {
this.dashboardInitService.init();
}
} }

View file

@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { WidgetsCollectionService } from '@app/event-emitter/dashboards/widgets-collection.service';
import { IssuesByTagsWidgetDataLoaderService } from './widget-data-loader/issues-by-tags.widget-data-loader.service';
import { createInteractiveWidget } from '@app/event-emitter/dashboards/interactive-widget-factory';
@Injectable()
export class DashboardInitService {
constructor(
private issuesByTagsWidgetDataLoaderService: IssuesByTagsWidgetDataLoaderService,
private widgetsCollectionService: WidgetsCollectionService,
) {}
init(): void {
const collection = [
createInteractiveWidget(
this.issuesByTagsWidgetDataLoaderService,
'kanban_by_tags',
),
createInteractiveWidget(
this.issuesByTagsWidgetDataLoaderService,
'issues_list_by_tags',
),
];
collection.forEach((w) => this.widgetsCollectionService.appendWidget(w));
}
}

View file

@ -1,7 +1,7 @@
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader'; import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
import { Controller, Get, Param, Render } from '@nestjs/common'; import { Controller, Get, Param, Render } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { IssuesByTagsWidgetService } from './widgets/issues-by-tags.widget.service'; import { IssuesByTagsWidgetDataLoaderService } from './widget-data-loader/issues-by-tags.widget-data-loader.service';
import { parse } from 'jsonc-parser'; import { parse } from 'jsonc-parser';
@Controller('simple-issues-list') @Controller('simple-issues-list')
@ -9,7 +9,7 @@ export class SimpleIssuesListController {
private path: string; private path: string;
constructor( constructor(
private issuesByTagsWidgetService: IssuesByTagsWidgetService, private issuesByTagsWidgetDataLoaderService: IssuesByTagsWidgetDataLoaderService,
private dynamicLoader: DynamicLoader, private dynamicLoader: DynamicLoader,
private configService: ConfigService, private configService: ConfigService,
) { ) {
@ -23,7 +23,7 @@ export class SimpleIssuesListController {
ext: 'jsonc', ext: 'jsonc',
parser: parse, parser: parse,
}); });
return await this.issuesByTagsWidgetService.render(cfg); return await this.issuesByTagsWidgetDataLoaderService.load(cfg);
} }
@Get('/by-tags/:name') @Get('/by-tags/:name')

View file

@ -1,18 +1,14 @@
import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader'; import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader';
import { RedmineEventsGateway } from '@app/event-emitter/events/redmine-events.gateway'; import { RedmineEventsGateway } from '@app/event-emitter/events/redmine-events.gateway';
import { IssuesService } from '@app/event-emitter/issues/issues.service'; import { IssuesService } from '@app/event-emitter/issues/issues.service';
import { ListIssuesByFieldsWidgetService } from '@app/event-emitter/project-dashboard/widgets/list-issues-by-fields.widget.service';
import { ListIssuesByUsersLikeJiraWidgetService } from '@app/event-emitter/project-dashboard/widgets/list-issues-by-users-like-jira.widget.service';
import { ListIssuesByUsersWidgetService } from '@app/event-emitter/project-dashboard/widgets/list-issues-by-users.widget.service';
import {
RootIssueSubTreesWidgetNs,
RootIssueSubTreesWidgetService,
} from '@app/event-emitter/project-dashboard/widgets/root-issue-subtrees.widget.service';
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
import { Controller, Get, Logger, Param, Render } from '@nestjs/common'; import { Controller, Get, Logger, Param, Render } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { parse } from 'jsonc-parser'; import { parse } from 'jsonc-parser';
import { IssuesByTagsWidgetService } from './widgets/issues-by-tags.widget.service'; import { IssuesByTagsWidgetDataLoaderService } from './widget-data-loader/issues-by-tags.widget-data-loader.service';
import { RootIssueSubTreesWidgetDataLoaderService } from '@app/event-emitter/dashboards/widget-data-loader/root-issue-subtrees.widget-data-loader.service';
import { ListIssuesByUsersWidgetDataLoaderService } from '@app/event-emitter/dashboards/widget-data-loader/list-issues-by-users.widget-data-loader.service';
import { ListIssuesByUsersLikeJiraWidgetDataLoaderService } from '@app/event-emitter/dashboards/widget-data-loader/list-issues-by-users-like-jira.widget-data-loader.service';
import { ListIssuesByFieldsWidgetDataLoaderService } from '@app/event-emitter/dashboards/widget-data-loader/list-issues-by-fields.widget-data-loader.service';
@Controller('simple-kanban-board') @Controller('simple-kanban-board')
export class SimpleKanbanBoardController { export class SimpleKanbanBoardController {
@ -20,14 +16,14 @@ export class SimpleKanbanBoardController {
private path: string; private path: string;
constructor( constructor(
private rootIssueSubTreesWidgetService: RootIssueSubTreesWidgetService, private rootIssueSubTreesWidgetDataLoaderService: RootIssueSubTreesWidgetDataLoaderService,
private dynamicLoader: DynamicLoader, private dynamicLoader: DynamicLoader,
private configService: ConfigService, private configService: ConfigService,
private listIssuesByUsersWidgetService: ListIssuesByUsersWidgetService, private listIssuesByUsersWidgetDataLoaderService: ListIssuesByUsersWidgetDataLoaderService,
private listIssuesByUsersLikeJiraWidgetService: ListIssuesByUsersLikeJiraWidgetService, private listIssuesByUsersLikeJiraWidgetDataLoaderService: ListIssuesByUsersLikeJiraWidgetDataLoaderService,
private issuesByTagsWidgetService: IssuesByTagsWidgetService, private issuesByTagsWidgetDataLoaderService: IssuesByTagsWidgetDataLoaderService,
private redmineEventsGateway: RedmineEventsGateway, private redmineEventsGateway: RedmineEventsGateway,
private listIssuesByFieldsWidgetService: ListIssuesByFieldsWidgetService, private listIssuesByFieldsWidgetDataLoaderService: ListIssuesByFieldsWidgetDataLoaderService,
private issuesService: IssuesService, private issuesService: IssuesService,
) { ) {
this.path = this.configService.get<string>('simpleKanbanBoard.path'); this.path = this.configService.get<string>('simpleKanbanBoard.path');
@ -40,7 +36,7 @@ export class SimpleKanbanBoardController {
ext: 'jsonc', ext: 'jsonc',
parser: parse, parser: parse,
}); });
return await this.rootIssueSubTreesWidgetService.render(cfg); return await this.rootIssueSubTreesWidgetDataLoaderService.load(cfg);
} }
@Get('/tree/:name') @Get('/tree/:name')
@ -73,7 +69,7 @@ export class SimpleKanbanBoardController {
ext: 'jsonc', ext: 'jsonc',
parser: parse, parser: parse,
}); });
return await this.listIssuesByUsersWidgetService.render(cfg); return await this.listIssuesByUsersWidgetDataLoaderService.load(cfg);
} }
@Get('/by-users/:name') @Get('/by-users/:name')
@ -89,7 +85,9 @@ export class SimpleKanbanBoardController {
ext: 'jsonc', ext: 'jsonc',
parser: parse, parser: parse,
}); });
return await this.listIssuesByUsersLikeJiraWidgetService.render(cfg); return await this.listIssuesByUsersLikeJiraWidgetDataLoaderService.load(
cfg,
);
} }
@Get('/by-users-like-jira/:name') @Get('/by-users-like-jira/:name')
@ -105,7 +103,7 @@ export class SimpleKanbanBoardController {
ext: 'jsonc', ext: 'jsonc',
parser: parse, parser: parse,
}); });
return await this.issuesByTagsWidgetService.render(cfg); return await this.issuesByTagsWidgetDataLoaderService.load(cfg);
} }
@Get('/by-tags/:name') @Get('/by-tags/:name')
@ -121,7 +119,7 @@ export class SimpleKanbanBoardController {
ext: 'jsonc', ext: 'jsonc',
parser: parse, parser: parse,
}); });
return await this.listIssuesByFieldsWidgetService.render(cfg); return await this.listIssuesByFieldsWidgetDataLoaderService.load(cfg);
} }
@Get('/by-fields/:name') @Get('/by-fields/:name')

View file

@ -6,13 +6,19 @@ import {
IssuesService, IssuesService,
IssuesServiceNs, IssuesServiceNs,
} from '@app/event-emitter/issues/issues.service'; } from '@app/event-emitter/issues/issues.service';
import { WidgetInterface } from '@app/event-emitter/project-dashboard/widget-interface';
import { FlatIssuesStore } from '@app/event-emitter/utils/flat-issues-store'; import { FlatIssuesStore } from '@app/event-emitter/utils/flat-issues-store';
import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from-object-by-key'; import { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from-object-by-key';
import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store'; import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import nano from 'nano'; import nano from 'nano';
import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer'; import * as PriorityStylesEnhancerNs from '@app/event-emitter/issue-enhancers/priority-styles-enhancer';
import { WidgetDataLoaderInterface } from '@app/event-emitter/dashboards/widget-data-loader-interface';
import {
AppError,
Result,
createAppError,
success,
} from '@app/event-emitter/utils/result';
export namespace IssuesByTagsWidgetNs { export namespace IssuesByTagsWidgetNs {
export type Params = { export type Params = {
@ -27,10 +33,10 @@ export namespace IssuesByTagsWidgetNs {
type Params = IssuesByTagsWidgetNs.Params; type Params = IssuesByTagsWidgetNs.Params;
@Injectable() @Injectable()
export class IssuesByTagsWidgetService export class IssuesByTagsWidgetDataLoaderService
implements WidgetInterface<Params, any, any> implements WidgetDataLoaderInterface<Params, any, any>
{ {
private logger = new Logger(IssuesByTagsWidgetService.name); private logger = new Logger(IssuesByTagsWidgetDataLoaderService.name);
private issuesLoader: IssuesServiceNs.IssuesLoader; private issuesLoader: IssuesServiceNs.IssuesLoader;
constructor( constructor(
@ -45,7 +51,7 @@ export class IssuesByTagsWidgetService
return true; return true;
} }
async render(widgetParams: Params): Promise<any> { async load(widgetParams: Params): Promise<Result<any, AppError>> {
let store: FlatIssuesStore; let store: FlatIssuesStore;
if (widgetParams.fromRootIssueId) { if (widgetParams.fromRootIssueId) {
store = await this.getListFromRoot(widgetParams.fromRootIssueId); store = await this.getListFromRoot(widgetParams.fromRootIssueId);
@ -54,7 +60,7 @@ export class IssuesByTagsWidgetService
} else { } else {
const errMsg = `Wrong widgetParams value`; const errMsg = `Wrong widgetParams value`;
this.logger.error(errMsg); this.logger.error(errMsg);
throw new Error(errMsg); return fail(createAppError(errMsg));
} }
await store.enhanceIssues([ await store.enhanceIssues([
this.timePassedHighlightEnhancer, this.timePassedHighlightEnhancer,
@ -100,7 +106,7 @@ export class IssuesByTagsWidgetService
return a.metainfo.title.localeCompare(b.metainfo.title); return a.metainfo.title.localeCompare(b.metainfo.title);
}); });
} }
return res; return success(res);
} }
private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> { private async getListFromRoot(issueId: number): Promise<FlatIssuesStore> {