From 0e6b64fb278d2c32b03ac1e6e45b0b2afdc8666e Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Tue, 19 Sep 2023 07:34:49 +0700 Subject: [PATCH 01/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20?= =?UTF-8?q?=D0=B4=D0=B0=D1=88=D0=B1=D0=BE=D1=80=D0=B4=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/main-config.jsonc.dist | 3 +- .../src/couchdb-datasources/dashboards.ts | 36 +++++++++++ .../src/dashboards/dashboards.service.ts | 56 +++++++++++++++++ .../event-emitter/src/event-emitter.module.ts | 14 ++++- libs/event-emitter/src/models/dashboard.ts | 4 ++ .../src/models/main-config-model.ts | 1 + libs/event-emitter/src/utils/result.ts | 61 +++++++++++++++++++ 7 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 libs/event-emitter/src/couchdb-datasources/dashboards.ts create mode 100644 libs/event-emitter/src/dashboards/dashboards.service.ts create mode 100644 libs/event-emitter/src/models/dashboard.ts create mode 100644 libs/event-emitter/src/utils/result.ts diff --git a/configs/main-config.jsonc.dist b/configs/main-config.jsonc.dist index e51d60d..6114f24 100644 --- a/configs/main-config.jsonc.dist +++ b/configs/main-config.jsonc.dist @@ -4,7 +4,8 @@ "changes": "", "userMetaInfo": "", "eccmDailyReports": "", - "eccmDailyReportsUserComments": "" + "eccmDailyReportsUserComments": "", + "dashboards": "" } }, "telegramBotToken": "", diff --git a/libs/event-emitter/src/couchdb-datasources/dashboards.ts b/libs/event-emitter/src/couchdb-datasources/dashboards.ts new file mode 100644 index 0000000..80aa5de --- /dev/null +++ b/libs/event-emitter/src/couchdb-datasources/dashboards.ts @@ -0,0 +1,36 @@ +import { Injectable, Logger } from '@nestjs/common'; +import nano from 'nano'; +import * as DashboardModel from '../models/dashboard'; +import { CouchDb } from './couchdb'; +import configuration from '../configs/main-config'; + +const config = configuration(); + +@Injectable() +export class Dashboards { + private static logger = new Logger(Dashboards.name); + private static dashboardsDb = null; + private static initialized = false; + + static async getDatasource(): Promise< + nano.DocumentScope + > { + if (Dashboards.initialized) { + return Dashboards.dashboardsDb; + } + Dashboards.initialized = true; + const n = CouchDb.getCouchDb(); + const dashboardsDbName = config.couchDb?.dbs?.dashboards; + const dbs = await n.db.list(); + if (!dbs.includes(dashboardsDbName)) { + await n.db.create(dashboardsDbName); + } + Dashboards.dashboardsDb = await n.db.use(dashboardsDbName); + Dashboards.logger.log(`Connected to dashboards db - ${dashboardsDbName}`); + return Dashboards.dashboardsDb; + } + + async getDatasource(): Promise> { + return await Dashboards.getDatasource(); + } +} diff --git a/libs/event-emitter/src/dashboards/dashboards.service.ts b/libs/event-emitter/src/dashboards/dashboards.service.ts new file mode 100644 index 0000000..98efd9e --- /dev/null +++ b/libs/event-emitter/src/dashboards/dashboards.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@nestjs/common'; +import { Dashboards as DashboardsDb } from '../couchdb-datasources/dashboards'; +import { Dashboard as DashboardModel } from '../models/dashboard'; +import nano from 'nano'; +import { randomUUID } from 'crypto'; +import { Result, fail, getOrThrow, success } from '../utils/result'; + +@Injectable() +export class DashboardsService { + constructor(private db: DashboardsDb) {} + + async create(name?: string): Promise> { + if (!name) { + name = randomUUID(); + } else if (await this.isExists(name)) { + return fail('ALREADY_EXISTS'); + } + const ds = await this.db.getDatasource(); + const doc: nano.MaybeDocument & DashboardModel = { + _id: name, + id: name, + data: null, + }; + await ds.insert(doc); + return success(await ds.get(name)); + } + + async load( + name: string, + ): Promise> { + const ds = await this.db.getDatasource(); + if (!(await this.isExists(name))) return fail('NOT_EXISTS'); + return success(await ds.get(name)); + } + + async isExists(name: string): Promise { + const ds = await this.db.getDatasource(); + try { + await ds.get(name); + return true; + } catch (ex) { + return false; + } + } + + async save(name: string, data: any): Promise> { + const ds = await this.db.getDatasource(); + const loadRes = await this.load(name); + if (loadRes.error) return fail(loadRes.error); + + const prevValue = getOrThrow(loadRes); + const newValue = { ...prevValue, data: data }; + await ds.insert(newValue); + return success(newValue); + } +} diff --git a/libs/event-emitter/src/event-emitter.module.ts b/libs/event-emitter/src/event-emitter.module.ts index 7383ec6..03ebfca 100644 --- a/libs/event-emitter/src/event-emitter.module.ts +++ b/libs/event-emitter/src/event-emitter.module.ts @@ -87,11 +87,14 @@ export class EventEmitterModule implements OnModuleInit { }, { provide: 'CALENDAR_SERVICE', - useFactory: (calendarEnhancer: CalendarEnhancer, issuesService: IssuesService): CalendarService => { + useFactory: ( + calendarEnhancer: CalendarEnhancer, + issuesService: IssuesService, + ): CalendarService => { const calendarEventsKey = calendarEnhancer.calendarEventsKey; return new CalendarService(calendarEventsKey, issuesService); }, - inject: ['CALENDAR_ENHANCER', IssuesService] + inject: ['CALENDAR_ENHANCER', IssuesService], }, ], exports: [ @@ -129,7 +132,12 @@ export class EventEmitterModule implements OnModuleInit { useExisting: 'CALENDAR_SERVICE', }, ], - controllers: [MainController, UsersController, IssuesController, CalendarController], + controllers: [ + MainController, + UsersController, + IssuesController, + CalendarController, + ], }; } diff --git a/libs/event-emitter/src/models/dashboard.ts b/libs/event-emitter/src/models/dashboard.ts new file mode 100644 index 0000000..ef4a812 --- /dev/null +++ b/libs/event-emitter/src/models/dashboard.ts @@ -0,0 +1,4 @@ +export type Dashboard = { + id: string; + data: any; +}; diff --git a/libs/event-emitter/src/models/main-config-model.ts b/libs/event-emitter/src/models/main-config-model.ts index b1c1342..3e5088a 100644 --- a/libs/event-emitter/src/models/main-config-model.ts +++ b/libs/event-emitter/src/models/main-config-model.ts @@ -16,6 +16,7 @@ export type MainConfigModel = { dbs: { users: string; issues: string; + dashboards: string; }; }; webhooks: WebhookConfigItemModel[]; diff --git a/libs/event-emitter/src/utils/result.ts b/libs/event-emitter/src/utils/result.ts new file mode 100644 index 0000000..945d2eb --- /dev/null +++ b/libs/event-emitter/src/utils/result.ts @@ -0,0 +1,61 @@ +export type Result = { + result?: T; + error?: E; +}; + +export function success(res: T): Result { + return { + result: res, + }; +} + +export function fail(error: E): Result { + return { + error: error, + }; +} + +export function getOrThrow(res: Result): T { + if (res.result) return res.result; + throw res.error ? res.error : 'UNKNOWN_ERROR'; +} + +export async function successOrError( + cb: () => Promise, +): Promise> { + try { + const res = await cb(); + return { + result: res, + }; + } catch (ex) { + return { + error: ex, + }; + } +} + +export type AppError = Error & { + app: true; +}; + +export function createAppError(msg: string): AppError { + const err = new Error(msg); + const app: AppError = { ...err, app: true }; + return app; +} + +export async function getOrAppErrorOrThrow( + fn: () => Promise, + onError: (err: AppError) => Promise, +): Promise { + try { + return await fn(); + } catch (ex) { + if (ex && ex.app) { + onError(ex); + return ex; + } + throw ex; + } +} From c8e71d3926de3c58fd562d017eb3e1d5e659a163 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Wed, 20 Sep 2023 10:12:45 +0700 Subject: [PATCH 02/15] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20CRUD=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B9=20=D1=81=20=D0=B4=D0=B0=D1=88=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=D0=B4=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/issue-event-emitter-config.jsonc.dist | 3 +- configs/main-config.jsonc.dist | 3 +- .../src/dashboards/dashboard.controller.ts | 38 +++++++++++++ .../src/dashboards/dashboards.service.ts | 57 +++++++++++++------ .../event-emitter/src/event-emitter.module.ts | 8 +++ libs/event-emitter/src/models/dashboard.ts | 4 +- libs/event-emitter/src/utils/result.ts | 33 +++++++---- src/app.module.ts | 2 + 8 files changed, 117 insertions(+), 31 deletions(-) create mode 100644 libs/event-emitter/src/dashboards/dashboard.controller.ts diff --git a/configs/issue-event-emitter-config.jsonc.dist b/configs/issue-event-emitter-config.jsonc.dist index d147a5b..3129594 100644 --- a/configs/issue-event-emitter-config.jsonc.dist +++ b/configs/issue-event-emitter-config.jsonc.dist @@ -43,7 +43,8 @@ "url": "", "dbs": { "users": "", - "issues": "" + "issues": "", + "dashboards": "" } } } \ No newline at end of file diff --git a/configs/main-config.jsonc.dist b/configs/main-config.jsonc.dist index 6114f24..e51d60d 100644 --- a/configs/main-config.jsonc.dist +++ b/configs/main-config.jsonc.dist @@ -4,8 +4,7 @@ "changes": "", "userMetaInfo": "", "eccmDailyReports": "", - "eccmDailyReportsUserComments": "", - "dashboards": "" + "eccmDailyReportsUserComments": "" } }, "telegramBotToken": "", diff --git a/libs/event-emitter/src/dashboards/dashboard.controller.ts b/libs/event-emitter/src/dashboards/dashboard.controller.ts new file mode 100644 index 0000000..b631554 --- /dev/null +++ b/libs/event-emitter/src/dashboards/dashboard.controller.ts @@ -0,0 +1,38 @@ +import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; +import { DashboardsService } from './dashboards.service'; +import { BadRequestErrorHandler, getOrAppErrorOrThrow } from '../utils/result'; + +@Controller('dashboard') +export class DashboardController { + constructor(private dashboardsService: DashboardsService) {} + + @Post(':name') + async create(@Param('name') name: string): Promise { + if (name === 'anonymous') { + name = null; + } + const res = await getOrAppErrorOrThrow( + () => this.dashboardsService.create(name), + BadRequestErrorHandler, + ); + return res.id; + } + + @Get(':name') + async load(@Param('name') name: string): Promise { + const res = await getOrAppErrorOrThrow( + () => this.dashboardsService.load(name), + BadRequestErrorHandler, + ); + return res; + } + + @Put(':name') + async save(@Param('name') name: string, @Body() data: any): Promise { + const res = await getOrAppErrorOrThrow( + () => this.dashboardsService.save(name, data), + BadRequestErrorHandler, + ); + return res; + } +} diff --git a/libs/event-emitter/src/dashboards/dashboards.service.ts b/libs/event-emitter/src/dashboards/dashboards.service.ts index 98efd9e..797be20 100644 --- a/libs/event-emitter/src/dashboards/dashboards.service.ts +++ b/libs/event-emitter/src/dashboards/dashboards.service.ts @@ -1,19 +1,28 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { Dashboards as DashboardsDb } from '../couchdb-datasources/dashboards'; -import { Dashboard as DashboardModel } from '../models/dashboard'; +import { + Dashboard as DashboardModel, + Data as DashboardData, +} from '../models/dashboard'; import nano from 'nano'; import { randomUUID } from 'crypto'; -import { Result, fail, getOrThrow, success } from '../utils/result'; +import { createAppError } from '../utils/result'; @Injectable() export class DashboardsService { + private logger = new Logger(DashboardsService.name); + constructor(private db: DashboardsDb) {} - async create(name?: string): Promise> { + async create(name?: string): Promise { if (!name) { name = randomUUID(); - } else if (await this.isExists(name)) { - return fail('ALREADY_EXISTS'); + } + this.logger.debug(`Create new dashboard with name - ${name}`); + if (await this.isExists(name)) { + const err = createAppError('ALREADY_EXISTS'); + this.logger.error(`Error - ${JSON.stringify(err)}`); + throw err; } const ds = await this.db.getDatasource(); const doc: nano.MaybeDocument & DashboardModel = { @@ -22,15 +31,23 @@ export class DashboardsService { data: null, }; await ds.insert(doc); - return success(await ds.get(name)); + return await ds.get(name); } - async load( + async loadRawData( name: string, - ): Promise> { + ): Promise { + this.logger.debug(`Load raw data, dashboard name - ${name}`); const ds = await this.db.getDatasource(); - if (!(await this.isExists(name))) return fail('NOT_EXISTS'); - return success(await ds.get(name)); + if (!(await this.isExists(name))) throw createAppError('NOT_EXISTS'); + const res = await ds.get(name); + return res; + } + + async load(name: string): Promise { + this.logger.debug(`Load dashboard name - ${name}`); + const rawData = await this.loadRawData(name); + return rawData.data; } async isExists(name: string): Promise { @@ -43,14 +60,20 @@ export class DashboardsService { } } - async save(name: string, data: any): Promise> { + async save(name: string, data: any): Promise { + this.logger.debug( + `Save dashboard name - ${name}, data - ${JSON.stringify(data)}`, + ); const ds = await this.db.getDatasource(); - const loadRes = await this.load(name); - if (loadRes.error) return fail(loadRes.error); + const prevValue = await this.loadRawData(name); - const prevValue = getOrThrow(loadRes); - const newValue = { ...prevValue, data: data }; + const newValue = { + _id: prevValue._id, + _rev: prevValue._rev, + id: prevValue.id, + data: data, + }; await ds.insert(newValue); - return success(newValue); + return; } } diff --git a/libs/event-emitter/src/event-emitter.module.ts b/libs/event-emitter/src/event-emitter.module.ts index 03ebfca..f1c9607 100644 --- a/libs/event-emitter/src/event-emitter.module.ts +++ b/libs/event-emitter/src/event-emitter.module.ts @@ -31,6 +31,9 @@ import { IssuesUpdaterService } from './issues-updater/issues-updater.service'; import { CalendarEnhancer } from './issue-enhancers/calendar-enhancer'; import { CalendarService } from './calendar/calendar.service'; import { CalendarController } from './calendar/calendar.controller'; +import { Dashboards as DashboardsDs } from './couchdb-datasources/dashboards'; +import { DashboardController } from './dashboards/dashboard.controller'; +import { DashboardsService } from './dashboards/dashboards.service'; @Module({}) export class EventEmitterModule implements OnModuleInit { @@ -48,6 +51,7 @@ export class EventEmitterModule implements OnModuleInit { CouchDb, Users, Issues, + DashboardsDs, RedmineUserCacheWriterService, UsersService, IssuesService, @@ -96,6 +100,7 @@ export class EventEmitterModule implements OnModuleInit { }, inject: ['CALENDAR_ENHANCER', IssuesService], }, + DashboardsService, ], exports: [ EventEmitterService, @@ -105,6 +110,7 @@ export class EventEmitterModule implements OnModuleInit { CouchDb, Users, Issues, + DashboardsDs, RedmineUserCacheWriterService, UsersService, IssuesService, @@ -131,12 +137,14 @@ export class EventEmitterModule implements OnModuleInit { provide: 'CALENDAR_SERVICE', useExisting: 'CALENDAR_SERVICE', }, + DashboardsService, ], controllers: [ MainController, UsersController, IssuesController, CalendarController, + DashboardController, ], }; } diff --git a/libs/event-emitter/src/models/dashboard.ts b/libs/event-emitter/src/models/dashboard.ts index ef4a812..8c19d37 100644 --- a/libs/event-emitter/src/models/dashboard.ts +++ b/libs/event-emitter/src/models/dashboard.ts @@ -1,4 +1,6 @@ +export type Data = Record | null; + export type Dashboard = { id: string; - data: any; + data: Data; }; diff --git a/libs/event-emitter/src/utils/result.ts b/libs/event-emitter/src/utils/result.ts index 945d2eb..fef53a4 100644 --- a/libs/event-emitter/src/utils/result.ts +++ b/libs/event-emitter/src/utils/result.ts @@ -1,3 +1,5 @@ +import { BadRequestException } from '@nestjs/common'; + export type Result = { result?: T; error?: E; @@ -39,23 +41,34 @@ export type AppError = Error & { app: true; }; -export function createAppError(msg: string): AppError { - const err = new Error(msg); - const app: AppError = { ...err, app: true }; - return app; +export function createAppError(msg: string | Error): AppError { + let err: any; + if (typeof msg === 'string') { + err = new Error(msg); + } else if (typeof msg === 'object') { + err = msg; + } else { + err = new Error('UNKNOWN_APP_ERROR'); + } + err.name = 'ApplicationError'; + return err; } export async function getOrAppErrorOrThrow( fn: () => Promise, - onError: (err: AppError) => Promise, -): Promise { + onAppError?: (err: Error) => Error, + onOtherError?: (err: Error) => Error, +): Promise { try { return await fn(); } catch (ex) { - if (ex && ex.app) { - onError(ex); - return ex; + if (ex && ex.name === 'ApplicationError') { + throw onAppError ? onAppError(ex) : ex; } - throw ex; + throw onOtherError ? onOtherError(ex) : ex; } } + +export function BadRequestErrorHandler(err: Error): Error { + return new BadRequestException(err.message); +} diff --git a/src/app.module.ts b/src/app.module.ts index 2afa718..221d5e8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -51,6 +51,7 @@ import { SimpleIssuesListController } from './dashboards/simple-issues-list.cont import { TagsManagerController } from './tags-manager/tags-manager.controller'; import { CreateTagManagerServiceProvider } from './tags-manager/tags-manager.service'; import { CalendarEnhancer } from '@app/event-emitter/issue-enhancers/calendar-enhancer'; +import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasources/dashboards'; @Module({ imports: [ @@ -145,6 +146,7 @@ export class AppModule implements OnModuleInit { UserMetaInfo.getDatasource(); DailyEccmReportsDatasource.getDatasource(); DailyEccmReportsUserCommentsDatasource.getDatasource(); + DashboardsDs.getDatasource(); this.enhancerService.addEnhancer([ this.timestampEnhancer, From 10b04bdb0fd05081716b3630dfdd1bf4b8714d06 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Mon, 2 Oct 2023 19:15:05 +0700 Subject: [PATCH 03/15] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20-=20=D1=80=D0=B0=D0=B7=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=B9=D0=B4=D0=B5=D1=80=D1=8B=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BE=D1=82=20=D0=B2=D0=B8=D0=B4=D0=B6=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D1=81=20=D0=BF=D1=80=D0=B5=D0=B4=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/dashboards/dashboard.controller.ts | 35 ++-- .../src/dashboards/dashboards-data.service.ts | 67 ++++++++ .../src/dashboards/dashboards.service.ts | 49 +++--- .../dashboards/interactive-widget-factory.ts | 28 ++++ .../src/dashboards/text-widget-factory.ts | 35 ++++ .../widget-data-loader-interface.ts | 9 ++ ...s-by-fields.widget-data-loader.service.ts} | 20 ++- ...s-like-jira.widget-data-loader.service.ts} | 24 ++- ...es-by-users.widget-data-loader.service.ts} | 20 ++- ...ue-subtrees.widget-data-loader.service.ts} | 12 +- .../src/dashboards/widget-interface.ts | 19 +++ .../dashboards/widgets-collection.service.ts | 68 ++++++++ .../event-emitter/src/event-emitter.module.ts | 33 ++-- libs/event-emitter/src/models/dashboard.ts | 25 ++- .../project-dashboard.service.ts | 152 ------------------ .../src/project-dashboard/widget-interface.ts | 4 - src/app.module.ts | 13 +- src/dashboards/dashboard-init.service.ts | 26 +++ .../simple-issues-list.controller.ts | 6 +- .../simple-kanban-board.controller.ts | 36 ++--- ...ues-by-tags.widget-data-loader.service.ts} | 20 ++- 21 files changed, 431 insertions(+), 270 deletions(-) create mode 100644 libs/event-emitter/src/dashboards/dashboards-data.service.ts create mode 100644 libs/event-emitter/src/dashboards/interactive-widget-factory.ts create mode 100644 libs/event-emitter/src/dashboards/text-widget-factory.ts create mode 100644 libs/event-emitter/src/dashboards/widget-data-loader-interface.ts rename libs/event-emitter/src/{project-dashboard/widgets/list-issues-by-fields.widget.service.ts => dashboards/widget-data-loader/list-issues-by-fields.widget-data-loader.service.ts} (89%) rename libs/event-emitter/src/{project-dashboard/widgets/list-issues-by-users-like-jira.widget.service.ts => dashboards/widget-data-loader/list-issues-by-users-like-jira.widget-data-loader.service.ts} (88%) rename libs/event-emitter/src/{project-dashboard/widgets/list-issues-by-users.widget.service.ts => dashboards/widget-data-loader/list-issues-by-users.widget-data-loader.service.ts} (89%) rename libs/event-emitter/src/{project-dashboard/widgets/root-issue-subtrees.widget.service.ts => dashboards/widget-data-loader/root-issue-subtrees.widget-data-loader.service.ts} (88%) create mode 100644 libs/event-emitter/src/dashboards/widget-interface.ts create mode 100644 libs/event-emitter/src/dashboards/widgets-collection.service.ts delete mode 100644 libs/event-emitter/src/project-dashboard/project-dashboard.service.ts delete mode 100644 libs/event-emitter/src/project-dashboard/widget-interface.ts create mode 100644 src/dashboards/dashboard-init.service.ts rename src/dashboards/{widgets/issues-by-tags.widget.service.ts => widget-data-loader/issues-by-tags.widget-data-loader.service.ts} (88%) diff --git a/libs/event-emitter/src/dashboards/dashboard.controller.ts b/libs/event-emitter/src/dashboards/dashboard.controller.ts index b631554..a8889e8 100644 --- a/libs/event-emitter/src/dashboards/dashboard.controller.ts +++ b/libs/event-emitter/src/dashboards/dashboard.controller.ts @@ -1,36 +1,45 @@ import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; import { DashboardsService } from './dashboards.service'; import { BadRequestErrorHandler, getOrAppErrorOrThrow } from '../utils/result'; +import { DashboardsDataService } from './dashboards-data.service'; @Controller('dashboard') export class DashboardController { - constructor(private dashboardsService: DashboardsService) {} + constructor( + private dashboardsService: DashboardsService, + private dashboardsDataService: DashboardsDataService, + ) {} - @Post(':name') - async create(@Param('name') name: string): Promise { - if (name === 'anonymous') { - name = null; - } + @Post() + async create(): Promise { const res = await getOrAppErrorOrThrow( - () => this.dashboardsService.create(name), + () => this.dashboardsService.create(), BadRequestErrorHandler, ); return res.id; } - @Get(':name') - async load(@Param('name') name: string): Promise { + @Get(':id') + async load(@Param('id') id: string): Promise { const res = await getOrAppErrorOrThrow( - () => this.dashboardsService.load(name), + () => this.dashboardsService.load(id), BadRequestErrorHandler, ); return res; } - @Put(':name') - async save(@Param('name') name: string, @Body() data: any): Promise { + @Get(':id/load-data') + async loadData(@Param('id') id: string): Promise { + return await getOrAppErrorOrThrow( + () => this.dashboardsDataService.loadData(id), + BadRequestErrorHandler, + ); + } + + @Put(':id') + async save(@Param('id') id: string, @Body() data: any): Promise { const res = await getOrAppErrorOrThrow( - () => this.dashboardsService.save(name, data), + () => this.dashboardsService.save(id, data), BadRequestErrorHandler, ); return res; diff --git a/libs/event-emitter/src/dashboards/dashboards-data.service.ts b/libs/event-emitter/src/dashboards/dashboards-data.service.ts new file mode 100644 index 0000000..4fab49f --- /dev/null +++ b/libs/event-emitter/src/dashboards/dashboards-data.service.ts @@ -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 { + 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> { + 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; + } +} diff --git a/libs/event-emitter/src/dashboards/dashboards.service.ts b/libs/event-emitter/src/dashboards/dashboards.service.ts index 797be20..997587a 100644 --- a/libs/event-emitter/src/dashboards/dashboards.service.ts +++ b/libs/event-emitter/src/dashboards/dashboards.service.ts @@ -1,9 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { Dashboards as DashboardsDb } from '../couchdb-datasources/dashboards'; -import { - Dashboard as DashboardModel, - Data as DashboardData, -} from '../models/dashboard'; +import * as DashboardModel from '../models/dashboard'; import nano from 'nano'; import { randomUUID } from 'crypto'; import { createAppError } from '../utils/result'; @@ -14,58 +11,56 @@ export class DashboardsService { constructor(private db: DashboardsDb) {} - async create(name?: string): Promise { - if (!name) { - name = randomUUID(); - } - this.logger.debug(`Create new dashboard with name - ${name}`); - if (await this.isExists(name)) { + async create(): Promise { + const id = randomUUID(); + this.logger.debug(`Create new dashboard with id - ${id}`); + if (await this.isExists(id)) { const err = createAppError('ALREADY_EXISTS'); this.logger.error(`Error - ${JSON.stringify(err)}`); throw err; } const ds = await this.db.getDatasource(); - const doc: nano.MaybeDocument & DashboardModel = { - _id: name, - id: name, + const doc: nano.MaybeDocument & DashboardModel.Dashboard = { + _id: id, + id: id, data: null, }; await ds.insert(doc); - return await ds.get(name); + return await ds.get(id); } async loadRawData( - name: string, - ): Promise { - this.logger.debug(`Load raw data, dashboard name - ${name}`); + id: string, + ): Promise { + this.logger.debug(`Load raw data, dashboard id - ${id}`); const ds = await this.db.getDatasource(); - if (!(await this.isExists(name))) throw createAppError('NOT_EXISTS'); - const res = await ds.get(name); + if (!(await this.isExists(id))) throw createAppError('NOT_EXISTS'); + const res = await ds.get(id); return res; } - async load(name: string): Promise { - this.logger.debug(`Load dashboard name - ${name}`); - const rawData = await this.loadRawData(name); + async load(id: string): Promise { + this.logger.debug(`Load dashboard id - ${id}`); + const rawData = await this.loadRawData(id); return rawData.data; } - async isExists(name: string): Promise { + async isExists(id: string): Promise { const ds = await this.db.getDatasource(); try { - await ds.get(name); + await ds.get(id); return true; } catch (ex) { return false; } } - async save(name: string, data: any): Promise { + async save(id: string, data: DashboardModel.Data): Promise { 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 prevValue = await this.loadRawData(name); + const prevValue = await this.loadRawData(id); const newValue = { _id: prevValue._id, diff --git a/libs/event-emitter/src/dashboards/interactive-widget-factory.ts b/libs/event-emitter/src/dashboards/interactive-widget-factory.ts new file mode 100644 index 0000000..b426f93 --- /dev/null +++ b/libs/event-emitter/src/dashboards/interactive-widget-factory.ts @@ -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 +{ + constructor( + public dataLoader: WidgetDataLoaderInterface, + public type: string, + ) {} + + async render( + widgetParams: any, + dataLoaderParams: any, + dashboardParams: any, + ): Promise> { + const data = await this.dataLoader.load(dataLoaderParams, dashboardParams); + return data; + } +} + +export function createInteractiveWidget( + dataLoader: WidgetDataLoaderInterface, + type: string, +): WidgetInterface { + return new InteractiveWidget(dataLoader, type); +} diff --git a/libs/event-emitter/src/dashboards/text-widget-factory.ts b/libs/event-emitter/src/dashboards/text-widget-factory.ts new file mode 100644 index 0000000..7e841c0 --- /dev/null +++ b/libs/event-emitter/src/dashboards/text-widget-factory.ts @@ -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 { + constructor( + public dataLoader: WidgetDataLoaderInterface, + public type: string, + public template: string, + ) {} + + async render( + widgetParams: any, + dataLoaderParams: any, + dashboardParams: any, + ): Promise> { + const params = { + widgetParams, + dataLoaderParams, + dashboardParams, + }; + const template = Handlebars.compile(this.template); + const res = template(params); + return success(res); + } +} + +export function createTextWidget( + dataLoader: WidgetDataLoaderInterface, + type: string, + template: string, +): WidgetInterface { + return new TextWidget(dataLoader, type, template); +} diff --git a/libs/event-emitter/src/dashboards/widget-data-loader-interface.ts b/libs/event-emitter/src/dashboards/widget-data-loader-interface.ts new file mode 100644 index 0000000..3608fc7 --- /dev/null +++ b/libs/event-emitter/src/dashboards/widget-data-loader-interface.ts @@ -0,0 +1,9 @@ +import { AppError, Result } from '../utils/result'; + +export interface WidgetDataLoaderInterface { + isMyConfig(dataLoaderParams: DLP): boolean; + load( + dataLoaderParams: DLP, + dashboardParams: DBP, + ): Promise>; +} diff --git a/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-fields.widget.service.ts b/libs/event-emitter/src/dashboards/widget-data-loader/list-issues-by-fields.widget-data-loader.service.ts similarity index 89% rename from libs/event-emitter/src/project-dashboard/widgets/list-issues-by-fields.widget.service.ts rename to libs/event-emitter/src/dashboards/widget-data-loader/list-issues-by-fields.widget-data-loader.service.ts index 3716036..7414c77 100644 --- a/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-fields.widget.service.ts +++ b/libs/event-emitter/src/dashboards/widget-data-loader/list-issues-by-fields.widget-data-loader.service.ts @@ -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 { Injectable, Logger } from '@nestjs/common'; import nano from 'nano'; -import { WidgetInterface } from '../widget-interface'; 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 type Params = { @@ -34,10 +40,10 @@ export namespace ListIssuesByFieldsWidgetNs { type Params = ListIssuesByFieldsWidgetNs.Params; @Injectable() -export class ListIssuesByFieldsWidgetService - implements WidgetInterface +export class ListIssuesByFieldsWidgetDataLoaderService + implements WidgetDataLoaderInterface { - private logger = new Logger(ListIssuesByFieldsWidgetService.name); + private logger = new Logger(ListIssuesByFieldsWidgetDataLoaderService.name); private issuesLoader: IssuesServiceNs.IssuesLoader; constructor( @@ -52,7 +58,7 @@ export class ListIssuesByFieldsWidgetService return true; } - async render(widgetParams: Params): Promise { + async load(widgetParams: Params): Promise> { let store: FlatIssuesStore; if (widgetParams.fromRootIssueId) { store = await this.getListFromRoot(widgetParams.fromRootIssueId); @@ -61,7 +67,7 @@ export class ListIssuesByFieldsWidgetService } else { const errMsg = `Wrong widgetParams value`; this.logger.error(errMsg); - throw new Error(errMsg); + return fail(createAppError(errMsg)); } await store.enhanceIssues([ this.timePassedHighlightEnhancer, @@ -87,7 +93,7 @@ export class ListIssuesByFieldsWidgetService return a.metainfo.title.localeCompare(b.metainfo.title); }); } - return res; + return success(res); } private async getListFromRoot(issueId: number): Promise { diff --git a/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users-like-jira.widget.service.ts b/libs/event-emitter/src/dashboards/widget-data-loader/list-issues-by-users-like-jira.widget-data-loader.service.ts similarity index 88% rename from libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users-like-jira.widget.service.ts rename to libs/event-emitter/src/dashboards/widget-data-loader/list-issues-by-users-like-jira.widget-data-loader.service.ts index d31eb6e..bd14553 100644 --- a/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users-like-jira.widget.service.ts +++ b/libs/event-emitter/src/dashboards/widget-data-loader/list-issues-by-users-like-jira.widget-data-loader.service.ts @@ -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 { Injectable, Logger } from '@nestjs/common'; import nano from 'nano'; -import { WidgetInterface } from '../widget-interface'; 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 Models { @@ -29,15 +35,17 @@ export namespace ListIssuesByUsersLikeJiraWidgetNs { type Params = ListIssuesByUsersLikeJiraWidgetNs.Models.Params; @Injectable() -export class ListIssuesByUsersLikeJiraWidgetService - implements WidgetInterface +export class ListIssuesByUsersLikeJiraWidgetDataLoaderService + implements WidgetDataLoaderInterface { - private logger = new Logger(ListIssuesByUsersLikeJiraWidgetService.name); + private logger = new Logger( + ListIssuesByUsersLikeJiraWidgetDataLoaderService.name, + ); private issuesLoader: IssuesServiceNs.IssuesLoader; constructor( - private issuesService: IssuesService, private timePassedHighlightEnhancer: TimePassedHighlightEnhancer, + private issuesService: IssuesService, private issueUrlEnhancer: IssueUrlEnhancer, ) { this.issuesLoader = this.issuesService.createDynamicIssuesLoader(); @@ -47,7 +55,7 @@ export class ListIssuesByUsersLikeJiraWidgetService return true; } - async render(widgetParams: Params): Promise { + async load(widgetParams: Params): Promise> { let store: FlatIssuesStore; if (widgetParams.fromRootIssueId) { store = await this.getListFromRoot(widgetParams.fromRootIssueId); @@ -56,7 +64,7 @@ export class ListIssuesByUsersLikeJiraWidgetService } else { const errMsg = `Wrong widgetParams value`; this.logger.error(errMsg); - throw new Error(errMsg); + return fail(createAppError(errMsg)); } await store.enhanceIssues([ this.timePassedHighlightEnhancer, @@ -92,7 +100,7 @@ export class ListIssuesByUsersLikeJiraWidgetService return a.metainfo.title.localeCompare(b.metainfo.title); }); } - return res; + return success(res); } private async getListFromRoot(issueId: number): Promise { diff --git a/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users.widget.service.ts b/libs/event-emitter/src/dashboards/widget-data-loader/list-issues-by-users.widget-data-loader.service.ts similarity index 89% rename from libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users.widget.service.ts rename to libs/event-emitter/src/dashboards/widget-data-loader/list-issues-by-users.widget-data-loader.service.ts index 4ba3cbd..6180254 100644 --- a/libs/event-emitter/src/project-dashboard/widgets/list-issues-by-users.widget.service.ts +++ b/libs/event-emitter/src/dashboards/widget-data-loader/list-issues-by-users.widget-data-loader.service.ts @@ -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 { Injectable, Logger } from '@nestjs/common'; import nano from 'nano'; -import { WidgetInterface } from '../widget-interface'; 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 Models { @@ -41,10 +47,10 @@ type ExtendedIssue = RedmineTypes.Issue & Record; type FindResult = ListIssuesByUsersWidgetNs.Models.FindResult; @Injectable() -export class ListIssuesByUsersWidgetService - implements WidgetInterface +export class ListIssuesByUsersWidgetDataLoaderService + implements WidgetDataLoaderInterface { - private logger = new Logger(ListIssuesByUsersWidgetService.name); + private logger = new Logger(ListIssuesByUsersWidgetDataLoaderService.name); private issuesLoader: IssuesServiceNs.IssuesLoader; constructor( @@ -59,7 +65,7 @@ export class ListIssuesByUsersWidgetService return true; } - async render(widgetParams: Params): Promise { + async load(widgetParams: Params): Promise> { let store: FlatIssuesStore; if (widgetParams.fromRootIssueId) { store = await this.getListFromRoot(widgetParams.fromRootIssueId); @@ -68,7 +74,7 @@ export class ListIssuesByUsersWidgetService } else { const errMsg = `Wrong widgetParams value`; this.logger.error(errMsg); - throw new Error(errMsg); + return fail(createAppError(errMsg)); } await store.enhanceIssues([ this.timePassedHighlightEnhancer, @@ -95,7 +101,7 @@ export class ListIssuesByUsersWidgetService return a.metainfo.title.localeCompare(b.metainfo.title); }); } - return res; + return success(res); } private async getListFromRoot(issueId: number): Promise { diff --git a/libs/event-emitter/src/project-dashboard/widgets/root-issue-subtrees.widget.service.ts b/libs/event-emitter/src/dashboards/widget-data-loader/root-issue-subtrees.widget-data-loader.service.ts similarity index 88% rename from libs/event-emitter/src/project-dashboard/widgets/root-issue-subtrees.widget.service.ts rename to libs/event-emitter/src/dashboards/widget-data-loader/root-issue-subtrees.widget-data-loader.service.ts index 94b7974..6288ee5 100644 --- a/libs/event-emitter/src/project-dashboard/widgets/root-issue-subtrees.widget.service.ts +++ b/libs/event-emitter/src/dashboards/widget-data-loader/root-issue-subtrees.widget-data-loader.service.ts @@ -11,8 +11,9 @@ import { TreeIssuesStoreNs, } from '@app/event-emitter/utils/tree-issues-store'; import { Injectable } from '@nestjs/common'; -import { WidgetInterface } from '../widget-interface'; 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 Models { @@ -39,8 +40,8 @@ export namespace RootIssueSubTreesWidgetNs { type Params = RootIssueSubTreesWidgetNs.Models.Params; @Injectable() -export class RootIssueSubTreesWidgetService - implements WidgetInterface +export class RootIssueSubTreesWidgetDataLoaderService + implements WidgetDataLoaderInterface { private issuesLoader: IssuesServiceNs.IssuesLoader; @@ -56,7 +57,7 @@ export class RootIssueSubTreesWidgetService return true; } - async render(widgetParams: Params): Promise { + async load(widgetParams: Params): Promise> { const treeStore = new TreeIssuesStore(); const rootIssue = await this.issuesService.getIssue( widgetParams.rootIssueId, @@ -89,11 +90,12 @@ export class RootIssueSubTreesWidgetService } } } - return stories.map((s) => { + const res = stories.map((s) => { return { data: s.store.groupByStatus(widgetParams.statuses), metainfo: s.metainfo, }; }); + return success(res); } } diff --git a/libs/event-emitter/src/dashboards/widget-interface.ts b/libs/event-emitter/src/dashboards/widget-interface.ts new file mode 100644 index 0000000..4fdd2c1 --- /dev/null +++ b/libs/event-emitter/src/dashboards/widget-interface.ts @@ -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 { + dataLoader: WidgetDataLoaderInterface; + type: string; + render( + widgetParams: WP, + dataLoaderParams: DLP, + dashboardParams: DBP, + ): Promise>; +} diff --git a/libs/event-emitter/src/dashboards/widgets-collection.service.ts b/libs/event-emitter/src/dashboards/widgets-collection.service.ts new file mode 100644 index 0000000..a09685a --- /dev/null +++ b/libs/event-emitter/src/dashboards/widgets-collection.service.ts @@ -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[] = []; + + 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, + ): Result { + 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, string> { + const widget = this.collection.find((w) => w.type === type); + return widget ? success(widget) : fail('WIDGET_WITH_SAME_TYPE_NOT_FOUND'); + } +} diff --git a/libs/event-emitter/src/event-emitter.module.ts b/libs/event-emitter/src/event-emitter.module.ts index f1c9607..0a6d2f5 100644 --- a/libs/event-emitter/src/event-emitter.module.ts +++ b/libs/event-emitter/src/event-emitter.module.ts @@ -18,15 +18,10 @@ import { IssuesService } from './issues/issues.service'; import { IssuesController } from './issues/issues.controller'; 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'; import { RedminePublicUrlConverter } from './converters/redmine-public-url.converter'; 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 { ListIssuesByFieldsWidgetService } from './project-dashboard/widgets/list-issues-by-fields.widget.service'; import { IssuesUpdaterService } from './issues-updater/issues-updater.service'; import { CalendarEnhancer } from './issue-enhancers/calendar-enhancer'; 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 { DashboardController } from './dashboards/dashboard.controller'; 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({}) export class EventEmitterModule implements OnModuleInit { @@ -57,15 +58,14 @@ export class EventEmitterModule implements OnModuleInit { IssuesService, TimestampEnhancer, EnhancerService, - ProjectDashboardService, - RootIssueSubTreesWidgetService, + RootIssueSubTreesWidgetDataLoaderService, DynamicLoader, RedminePublicUrlConverter, IssueUrlEnhancer, - ListIssuesByUsersWidgetService, - ListIssuesByUsersLikeJiraWidgetService, + ListIssuesByUsersWidgetDataLoaderService, + ListIssuesByUsersLikeJiraWidgetDataLoaderService, TimePassedHighlightEnhancer, - ListIssuesByFieldsWidgetService, + ListIssuesByFieldsWidgetDataLoaderService, { provide: 'ISSUES_UPDATER_SERVICE', useFactory: (configService: ConfigService) => { @@ -101,6 +101,8 @@ export class EventEmitterModule implements OnModuleInit { inject: ['CALENDAR_ENHANCER', IssuesService], }, DashboardsService, + DashboardsDataService, + WidgetsCollectionService, ], exports: [ EventEmitterService, @@ -116,15 +118,14 @@ export class EventEmitterModule implements OnModuleInit { IssuesService, TimestampEnhancer, EnhancerService, - ProjectDashboardService, - RootIssueSubTreesWidgetService, + RootIssueSubTreesWidgetDataLoaderService, DynamicLoader, RedminePublicUrlConverter, IssueUrlEnhancer, - ListIssuesByUsersWidgetService, - ListIssuesByUsersLikeJiraWidgetService, + ListIssuesByUsersWidgetDataLoaderService, + ListIssuesByUsersLikeJiraWidgetDataLoaderService, TimePassedHighlightEnhancer, - ListIssuesByFieldsWidgetService, + ListIssuesByFieldsWidgetDataLoaderService, { provide: 'ISSUES_UPDATER_SERVICE', useExisting: 'ISSUES_UPDATER_SERVICE', @@ -138,6 +139,8 @@ export class EventEmitterModule implements OnModuleInit { useExisting: 'CALENDAR_SERVICE', }, DashboardsService, + DashboardsDataService, + WidgetsCollectionService, ], controllers: [ MainController, diff --git a/libs/event-emitter/src/models/dashboard.ts b/libs/event-emitter/src/models/dashboard.ts index 8c19d37..8f28995 100644 --- a/libs/event-emitter/src/models/dashboard.ts +++ b/libs/event-emitter/src/models/dashboard.ts @@ -1,6 +1,29 @@ -export type Data = Record | null; +export type Data = { + widgets: Widget[]; + title: string; +} | null; export type Dashboard = { id: string; data: Data; }; + +/** + * Параметры для отрисовки данных + */ +export type WidgetParams = { + collapsed?: boolean; +} & Record; + +/** + * Параметры для загрузки данных + */ +export type DataLoaderParams = Record | null; + +export type Widget = { + type: string; + id: string; + title: string; + widgetParams?: WidgetParams; + dataLoaderParams?: DataLoaderParams; +}; diff --git a/libs/event-emitter/src/project-dashboard/project-dashboard.service.ts b/libs/event-emitter/src/project-dashboard/project-dashboard.service.ts deleted file mode 100644 index 78446ff..0000000 --- a/libs/event-emitter/src/project-dashboard/project-dashboard.service.ts +++ /dev/null @@ -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; - }; - export type AnyFilterParams = Tree | List | DynamicList | Version; - } - - export type FilterParams = Record; - - 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[]; - - 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; - } -} diff --git a/libs/event-emitter/src/project-dashboard/widget-interface.ts b/libs/event-emitter/src/project-dashboard/widget-interface.ts deleted file mode 100644 index 2f00e79..0000000 --- a/libs/event-emitter/src/project-dashboard/widget-interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface WidgetInterface { - isMyConfig(widgetParams: W): boolean; - render(widgetParams: W, dashboardParams: D): Promise; -} diff --git a/src/app.module.ts b/src/app.module.ts index 221d5e8..077e588 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -43,7 +43,7 @@ import { SetDailyEccmUserCommentBotHandlerService } from './telegram-bot/handler import { DailyEccmWithExtraDataService } from './reports/daily-eccm-with-extra-data.service'; import { SimpleKanbanBoardController } from './dashboards/simple-kanban-board.controller'; 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 { ServeStaticModule } from '@nestjs/serve-static'; 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 { CalendarEnhancer } from '@app/event-emitter/issue-enhancers/calendar-enhancer'; import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasources/dashboards'; +import { DashboardInitService } from './dashboards/dashboard-init.service'; @Module({ imports: [ @@ -99,7 +100,7 @@ import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasourc DailyEccmUserCommentsService, SetDailyEccmUserCommentBotHandlerService, DailyEccmWithExtraDataService, - IssuesByTagsWidgetService, + IssuesByTagsWidgetDataLoaderService, { provide: 'CATEGORY_MERGE_TO_TAGS_ENHANCER', useFactory: (configService: ConfigService) => { @@ -111,6 +112,7 @@ import { Dashboards as DashboardsDs } from '@app/event-emitter/couchdb-datasourc inject: [ConfigService], }, CreateTagManagerServiceProvider('TAG_MANAGER_SERVICE'), + DashboardInitService, ], }) export class AppModule implements OnModuleInit { @@ -137,6 +139,8 @@ export class AppModule implements OnModuleInit { @Inject('CALENDAR_ENHANCER') private calendarEnhancer: CalendarEnhancer, + + private dashboardInitService: DashboardInitService, ) {} onModuleInit() { @@ -215,6 +219,7 @@ export class AppModule implements OnModuleInit { }); this.initDailyEccmUserCommentsPipeline(); + this.initDashbordProviders(); } private initDailyEccmUserCommentsPipeline(): void { @@ -228,4 +233,8 @@ export class AppModule implements OnModuleInit { }, ); } + + private initDashbordProviders(): void { + this.dashboardInitService.init(); + } } diff --git a/src/dashboards/dashboard-init.service.ts b/src/dashboards/dashboard-init.service.ts new file mode 100644 index 0000000..cb3e7f1 --- /dev/null +++ b/src/dashboards/dashboard-init.service.ts @@ -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)); + } +} diff --git a/src/dashboards/simple-issues-list.controller.ts b/src/dashboards/simple-issues-list.controller.ts index 5a91041..705b326 100644 --- a/src/dashboards/simple-issues-list.controller.ts +++ b/src/dashboards/simple-issues-list.controller.ts @@ -1,7 +1,7 @@ import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader'; import { Controller, Get, Param, Render } from '@nestjs/common'; 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'; @Controller('simple-issues-list') @@ -9,7 +9,7 @@ export class SimpleIssuesListController { private path: string; constructor( - private issuesByTagsWidgetService: IssuesByTagsWidgetService, + private issuesByTagsWidgetDataLoaderService: IssuesByTagsWidgetDataLoaderService, private dynamicLoader: DynamicLoader, private configService: ConfigService, ) { @@ -23,7 +23,7 @@ export class SimpleIssuesListController { ext: 'jsonc', parser: parse, }); - return await this.issuesByTagsWidgetService.render(cfg); + return await this.issuesByTagsWidgetDataLoaderService.load(cfg); } @Get('/by-tags/:name') diff --git a/src/dashboards/simple-kanban-board.controller.ts b/src/dashboards/simple-kanban-board.controller.ts index 0bed6b5..a504923 100644 --- a/src/dashboards/simple-kanban-board.controller.ts +++ b/src/dashboards/simple-kanban-board.controller.ts @@ -1,18 +1,14 @@ import { DynamicLoader } from '@app/event-emitter/configs/dynamic-loader'; import { RedmineEventsGateway } from '@app/event-emitter/events/redmine-events.gateway'; 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 { ConfigService } from '@nestjs/config'; 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') export class SimpleKanbanBoardController { @@ -20,14 +16,14 @@ export class SimpleKanbanBoardController { private path: string; constructor( - private rootIssueSubTreesWidgetService: RootIssueSubTreesWidgetService, + private rootIssueSubTreesWidgetDataLoaderService: RootIssueSubTreesWidgetDataLoaderService, private dynamicLoader: DynamicLoader, private configService: ConfigService, - private listIssuesByUsersWidgetService: ListIssuesByUsersWidgetService, - private listIssuesByUsersLikeJiraWidgetService: ListIssuesByUsersLikeJiraWidgetService, - private issuesByTagsWidgetService: IssuesByTagsWidgetService, + private listIssuesByUsersWidgetDataLoaderService: ListIssuesByUsersWidgetDataLoaderService, + private listIssuesByUsersLikeJiraWidgetDataLoaderService: ListIssuesByUsersLikeJiraWidgetDataLoaderService, + private issuesByTagsWidgetDataLoaderService: IssuesByTagsWidgetDataLoaderService, private redmineEventsGateway: RedmineEventsGateway, - private listIssuesByFieldsWidgetService: ListIssuesByFieldsWidgetService, + private listIssuesByFieldsWidgetDataLoaderService: ListIssuesByFieldsWidgetDataLoaderService, private issuesService: IssuesService, ) { this.path = this.configService.get('simpleKanbanBoard.path'); @@ -40,7 +36,7 @@ export class SimpleKanbanBoardController { ext: 'jsonc', parser: parse, }); - return await this.rootIssueSubTreesWidgetService.render(cfg); + return await this.rootIssueSubTreesWidgetDataLoaderService.load(cfg); } @Get('/tree/:name') @@ -73,7 +69,7 @@ export class SimpleKanbanBoardController { ext: 'jsonc', parser: parse, }); - return await this.listIssuesByUsersWidgetService.render(cfg); + return await this.listIssuesByUsersWidgetDataLoaderService.load(cfg); } @Get('/by-users/:name') @@ -89,7 +85,9 @@ export class SimpleKanbanBoardController { ext: 'jsonc', parser: parse, }); - return await this.listIssuesByUsersLikeJiraWidgetService.render(cfg); + return await this.listIssuesByUsersLikeJiraWidgetDataLoaderService.load( + cfg, + ); } @Get('/by-users-like-jira/:name') @@ -105,7 +103,7 @@ export class SimpleKanbanBoardController { ext: 'jsonc', parser: parse, }); - return await this.issuesByTagsWidgetService.render(cfg); + return await this.issuesByTagsWidgetDataLoaderService.load(cfg); } @Get('/by-tags/:name') @@ -121,7 +119,7 @@ export class SimpleKanbanBoardController { ext: 'jsonc', parser: parse, }); - return await this.listIssuesByFieldsWidgetService.render(cfg); + return await this.listIssuesByFieldsWidgetDataLoaderService.load(cfg); } @Get('/by-fields/:name') diff --git a/src/dashboards/widgets/issues-by-tags.widget.service.ts b/src/dashboards/widget-data-loader/issues-by-tags.widget-data-loader.service.ts similarity index 88% rename from src/dashboards/widgets/issues-by-tags.widget.service.ts rename to src/dashboards/widget-data-loader/issues-by-tags.widget-data-loader.service.ts index 13e2e9c..bca896f 100644 --- a/src/dashboards/widgets/issues-by-tags.widget.service.ts +++ b/src/dashboards/widget-data-loader/issues-by-tags.widget-data-loader.service.ts @@ -6,13 +6,19 @@ import { IssuesService, IssuesServiceNs, } 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 { GetValueFromObjectByKey } from '@app/event-emitter/utils/get-value-from-object-by-key'; import { TreeIssuesStore } from '@app/event-emitter/utils/tree-issues-store'; import { Injectable, Logger } from '@nestjs/common'; import nano from 'nano'; 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 type Params = { @@ -27,10 +33,10 @@ export namespace IssuesByTagsWidgetNs { type Params = IssuesByTagsWidgetNs.Params; @Injectable() -export class IssuesByTagsWidgetService - implements WidgetInterface +export class IssuesByTagsWidgetDataLoaderService + implements WidgetDataLoaderInterface { - private logger = new Logger(IssuesByTagsWidgetService.name); + private logger = new Logger(IssuesByTagsWidgetDataLoaderService.name); private issuesLoader: IssuesServiceNs.IssuesLoader; constructor( @@ -45,7 +51,7 @@ export class IssuesByTagsWidgetService return true; } - async render(widgetParams: Params): Promise { + async load(widgetParams: Params): Promise> { let store: FlatIssuesStore; if (widgetParams.fromRootIssueId) { store = await this.getListFromRoot(widgetParams.fromRootIssueId); @@ -54,7 +60,7 @@ export class IssuesByTagsWidgetService } else { const errMsg = `Wrong widgetParams value`; this.logger.error(errMsg); - throw new Error(errMsg); + return fail(createAppError(errMsg)); } await store.enhanceIssues([ this.timePassedHighlightEnhancer, @@ -100,7 +106,7 @@ export class IssuesByTagsWidgetService return a.metainfo.title.localeCompare(b.metainfo.title); }); } - return res; + return success(res); } private async getListFromRoot(issueId: number): Promise { From 14e0108f028bdadfdd6aca4fbcd0c9bc0a76b7ee Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Tue, 3 Oct 2023 07:04:15 +0700 Subject: [PATCH 04/15] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=B0?= =?UTF-8?q?=20=D0=B5=D0=BD=D0=B4=D0=BF=D0=BE=D0=B8=D0=BD=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=B2=D1=80=D0=B0=D1=89=D0=B0=D0=B5=D0=BC=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8=D0=B7=20=D0=B2?= =?UTF-8?q?=D0=B8=D0=B4=D0=B6=D0=B5=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/dashboards/dashboard.controller.ts | 2 +- .../src/dashboards/dashboards-data.service.ts | 14 +++----------- .../src/dashboards/interactive-widget-factory.ts | 10 ++++++++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/libs/event-emitter/src/dashboards/dashboard.controller.ts b/libs/event-emitter/src/dashboards/dashboard.controller.ts index a8889e8..013f769 100644 --- a/libs/event-emitter/src/dashboards/dashboard.controller.ts +++ b/libs/event-emitter/src/dashboards/dashboard.controller.ts @@ -3,7 +3,7 @@ import { DashboardsService } from './dashboards.service'; import { BadRequestErrorHandler, getOrAppErrorOrThrow } from '../utils/result'; import { DashboardsDataService } from './dashboards-data.service'; -@Controller('dashboard') +@Controller('api/dashboard') export class DashboardController { constructor( private dashboardsService: DashboardsService, diff --git a/libs/event-emitter/src/dashboards/dashboards-data.service.ts b/libs/event-emitter/src/dashboards/dashboards-data.service.ts index 4fab49f..74fe340 100644 --- a/libs/event-emitter/src/dashboards/dashboards-data.service.ts +++ b/libs/event-emitter/src/dashboards/dashboards-data.service.ts @@ -1,13 +1,7 @@ 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 { AppError, Result, createAppError, fail } from '../utils/result'; import { WidgetsCollectionService } from './widgets-collection.service'; export type WidgetWithData = { @@ -38,10 +32,8 @@ export class DashboardsDataService { ); if (loadRes.result) { isSuccess = true; - results.push({ - widget: widget, - data: loadRes.result, - }); + loadRes.result.widgetId = widget.id; + results.push(loadRes.result); } } if (!isSuccess) throw createAppError('CANNOT_LOAD_DATA'); diff --git a/libs/event-emitter/src/dashboards/interactive-widget-factory.ts b/libs/event-emitter/src/dashboards/interactive-widget-factory.ts index b426f93..7e8876e 100644 --- a/libs/event-emitter/src/dashboards/interactive-widget-factory.ts +++ b/libs/event-emitter/src/dashboards/interactive-widget-factory.ts @@ -1,4 +1,4 @@ -import { Result, AppError } from '../utils/result'; +import { Result, AppError, fail, success } from '../utils/result'; import { WidgetDataLoaderInterface } from './widget-data-loader-interface'; import { WidgetInterface } from './widget-interface'; @@ -16,7 +16,13 @@ export class InteractiveWidget dashboardParams: any, ): Promise> { const data = await this.dataLoader.load(dataLoaderParams, dashboardParams); - return data; + return data.error + ? fail(data.error) + : success({ + data: data.result, + widgetParams: widgetParams, + dashboardParams: dashboardParams, + }); } } From 0e28eba615e1fbb939290916a2809924a55687b2 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Tue, 3 Oct 2023 07:34:56 +0700 Subject: [PATCH 05/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86?= =?UTF-8?q?=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=B0=D1=88=D0=B1=D0=BE=D1=80=D0=B4=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D0=B4=D0=B0=D1=88=D0=B1=D0=BE=D1=80=D0=B4?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/.eslintrc.js | 24 +++++++++ frontend/src/dashboard/dashboard-page.tsx | 8 +++ frontend/src/dashboard/dashboards-page.tsx | 6 +++ .../src/kanban-board/kanban-boards-page.tsx | 37 +++++++------ frontend/src/router.tsx | 54 +++++++++++-------- 5 files changed, 91 insertions(+), 38 deletions(-) create mode 100644 frontend/src/.eslintrc.js create mode 100644 frontend/src/dashboard/dashboard-page.tsx create mode 100644 frontend/src/dashboard/dashboards-page.tsx diff --git a/frontend/src/.eslintrc.js b/frontend/src/.eslintrc.js new file mode 100644 index 0000000..f6c62be --- /dev/null +++ b/frontend/src/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/frontend/src/dashboard/dashboard-page.tsx b/frontend/src/dashboard/dashboard-page.tsx new file mode 100644 index 0000000..fe362e1 --- /dev/null +++ b/frontend/src/dashboard/dashboard-page.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +export const DashboardPage = (): JSX.Element => { + const params = useParams(); + const id = params.id as string; + return

Dashboard

; +}; diff --git a/frontend/src/dashboard/dashboards-page.tsx b/frontend/src/dashboard/dashboards-page.tsx new file mode 100644 index 0000000..49b0110 --- /dev/null +++ b/frontend/src/dashboard/dashboards-page.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + +export const DashboardsPage = (): JSX.Element => { + // TODO: code for DashboardsPage + return

Dashboards

; +}; diff --git a/frontend/src/kanban-board/kanban-boards-page.tsx b/frontend/src/kanban-board/kanban-boards-page.tsx index f9ba580..ca716e0 100644 --- a/frontend/src/kanban-board/kanban-boards-page.tsx +++ b/frontend/src/kanban-board/kanban-boards-page.tsx @@ -4,19 +4,24 @@ import * as Stores from './store'; import * as KBS from './kanban-boards'; export const KanbanBoardsPage = (): JSX.Element => { - const params = useParams(); - const name = params.name as string; - const type = params.type as string; - - // DEBUG: begin - console.debug(`KanbanBoardsPage: type=${type}; name=${name}`); - useEffect(() => { - console.debug(`KanbanBoardsPage: type=${type}; name=${name}`); - }); - // DEBUG: end - - const store = Stores.PageStore.create({loaded: false, type: type, name: name, data: null}); - Stores.PageStoreLoadData(store); - - return ; -} \ No newline at end of file + const params = useParams(); + const name = params.name as string; + const type = params.type as string; + + // DEBUG: begin + console.debug(`KanbanBoardsPage: type=${type}; name=${name}`); + useEffect(() => { + console.debug(`KanbanBoardsPage: type=${type}; name=${name}`); + }); + // DEBUG: end + + const store = Stores.PageStore.create({ + loaded: false, + type: type, + name: name, + data: null, + }); + Stores.PageStoreLoadData(store); + + return ; +}; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 890097e..c9642c0 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -1,25 +1,35 @@ -import React from "react"; -import { createBrowserRouter } from "react-router-dom"; -import StartPage from "./start-page/start-page"; -import UnknownPage from "./unknown-page"; -import { KanbanBoardsPage } from "./kanban-board/kanban-boards-page"; -import { IssuesListBoardPage } from "./issues-list-board/issues-list-boards-page"; +import React from 'react'; +import { createBrowserRouter } from 'react-router-dom'; +import StartPage from './start-page/start-page'; +import UnknownPage from './unknown-page'; +import { KanbanBoardsPage } from './kanban-board/kanban-boards-page'; +import { IssuesListBoardPage } from './issues-list-board/issues-list-boards-page'; +import { DashboardsPage } from './dashboard/dashboards-page'; +import { DashboardPage } from './dashboard/dashboard-page'; export const router = createBrowserRouter([ - { - path: "/", - element: (), + { + path: '/', + element: , }, - { - path: "/kanban-board/:type/:name", - element: () - }, - { - path: "/issues-list-board/:type/:name", - element: () - }, - { - path: "*", - element: () - } -]); \ No newline at end of file + { + path: '/kanban-board/:type/:name', + element: , + }, + { + path: '/issues-list-board/:type/:name', + element: , + }, + { + path: '/dashboards', + element: , + }, + { + path: '/dashboard/:id', + element: , + }, + { + path: '*', + element: , + }, +]); From 0b82ca564ab1cd60eb0815d8a4fcf470eb330342 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Tue, 3 Oct 2023 07:48:57 +0700 Subject: [PATCH 06/15] =?UTF-8?q?=D0=A4=D0=BE=D1=80=D0=BC=D0=B0=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20frontend-?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=80=D0=B8=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=BE?= =?UTF-8?q?=20=D0=BA=20=D0=BE=D0=B1=D1=89=D0=B8=D0=BC=20=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=D0=B0=D0=BC=20=D1=81=20backend-=D0=BE=D0=BC?= =?UTF-8?q?=20=D1=81=20=D0=BF=D0=BE=D0=BC=D0=BE=D1=89=D1=8C=D1=8E=20eslint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/index.tsx | 4 +- .../issues-list-board/issues-list-board.tsx | 56 ++--- .../issues-list-boards-page.tsx | 38 ++-- .../issues-list-board/issues-list-boards.tsx | 60 +++--- .../issues-list-board/issues-list-card.tsx | 91 ++++---- frontend/src/issues-list-board/store.ts | 149 +++++++------ frontend/src/kanban-board/column.tsx | 33 +-- frontend/src/kanban-board/kanban-board.tsx | 42 ++-- frontend/src/kanban-board/kanban-boards.tsx | 84 ++++---- frontend/src/kanban-board/kanban-card.tsx | 116 +++++----- frontend/src/kanban-board/store.ts | 196 ++++++++--------- .../misc-components/issue-details-dialog.tsx | 203 +++++++++--------- frontend/src/misc-components/issue-href.tsx | 18 +- frontend/src/misc-components/tag.tsx | 24 +-- frontend/src/misc-components/tags.tsx | 36 ++-- frontend/src/misc-components/time-passed.tsx | 70 +++--- .../src/misc-components/top-right-menu.tsx | 99 +++++---- .../src/misc-components/unreaded-flag.tsx | 116 +++++----- frontend/src/start-page/basement.tsx | 29 ++- frontend/src/start-page/content-block.tsx | 18 +- frontend/src/start-page/content.tsx | 10 +- frontend/src/start-page/cover.tsx | 32 +-- .../src/start-page/notification-block.tsx | 44 ++-- frontend/src/start-page/start-page.tsx | 201 ++++++++++------- frontend/src/start-page/top-bar.tsx | 57 +++-- frontend/src/unknown-page.tsx | 6 +- .../src/utils/service-actions-buttons.tsx | 15 +- frontend/src/utils/service-actions.ts | 40 ++-- frontend/src/utils/spent-hours-to-fixed.ts | 18 +- frontend/src/utils/style.ts | 40 ++-- frontend/src/utils/unreaded-provider.ts | 26 +-- 31 files changed, 1079 insertions(+), 892 deletions(-) diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 032464f..07924fc 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -5,12 +5,12 @@ import App from './App'; import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById('root') as HTMLElement, ); root.render( - + , ); // If you want to start measuring performance in your app, pass a function diff --git a/frontend/src/issues-list-board/issues-list-board.tsx b/frontend/src/issues-list-board/issues-list-board.tsx index eb51b8c..01ba1c5 100644 --- a/frontend/src/issues-list-board/issues-list-board.tsx +++ b/frontend/src/issues-list-board/issues-list-board.tsx @@ -5,32 +5,34 @@ import Css from './issues-list-board.module.css'; import * as IssuesListCardNs from './issues-list-card'; export type Props = { - store: IBoardStore -} + store: IBoardStore; +}; export const IssuesListBoard = observer((props: Props): JSX.Element => { - const list: JSX.Element[] = props.store.data.map((issue) => { - return ( - - ); - }); - let title: JSX.Element; - if (props.store.metainfo.url) { - title = {props.store.metainfo.title}; - } else { - title = <>{props.store.metainfo.title}; - } - return ( -
-
-

{title}

- - anchor - -
-
- {list} -
-
- ); -}); \ No newline at end of file + const list: JSX.Element[] = props.store.data.map((issue) => { + return ; + }); + let title: JSX.Element; + if (props.store.metainfo.url) { + title = {props.store.metainfo.title}; + } else { + title = <>{props.store.metainfo.title}; + } + return ( +
+
+

+ {title} +

+ + anchor + +
+
{list}
+
+ ); +}); diff --git a/frontend/src/issues-list-board/issues-list-boards-page.tsx b/frontend/src/issues-list-board/issues-list-boards-page.tsx index 531ad27..0474c63 100644 --- a/frontend/src/issues-list-board/issues-list-boards-page.tsx +++ b/frontend/src/issues-list-board/issues-list-boards-page.tsx @@ -4,21 +4,23 @@ import * as IssuesListStoreNs from './store'; import * as IssuesListBoardsNs from './issues-list-boards'; export const IssuesListBoardPage = (): JSX.Element => { - const params = useParams(); - const name = params.name as string; - const type = params.type as string; - - // DEBUG: begin - console.debug(`Issues list page: type=${type}; name=${name}`); - useEffect(() => { - console.debug(`Issues list page: type=${type}; name=${name}`); - }); - // DEBUG: end - - const store = IssuesListStoreNs.PageStore.create({loaded: false, type: type, name: name}); - IssuesListStoreNs.PageStoreLoadData(store); - - return ( - - ); -}; \ No newline at end of file + const params = useParams(); + const name = params.name as string; + const type = params.type as string; + + // DEBUG: begin + console.debug(`Issues list page: type=${type}; name=${name}`); + useEffect(() => { + console.debug(`Issues list page: type=${type}; name=${name}`); + }); + // DEBUG: end + + const store = IssuesListStoreNs.PageStore.create({ + loaded: false, + type: type, + name: name, + }); + IssuesListStoreNs.PageStoreLoadData(store); + + return ; +}; diff --git a/frontend/src/issues-list-board/issues-list-boards.tsx b/frontend/src/issues-list-board/issues-list-boards.tsx index c9250bb..b77c2db 100644 --- a/frontend/src/issues-list-board/issues-list-boards.tsx +++ b/frontend/src/issues-list-board/issues-list-boards.tsx @@ -7,35 +7,37 @@ import { SetIssuesReadingTimestamp } from '../utils/unreaded-provider'; import * as ServiceActionsButtons from '../utils/service-actions-buttons'; export type Props = { - store: IssuesListBoardStore.IPageStore + store: IssuesListBoardStore.IPageStore; }; export const IssuesListBoards = observer((props: Props): JSX.Element => { - const data = props.store.data; - if (!props.store.loaded || !data) { - return
Loading...
- } - const list: any[] = []; - for (let i = 0; i < data.length; i++) { - const boardData = data[i]; - const key = boardData.metainfo.title; - const board = - list.push(board); - } - const topRightMenuStore = TopRightMenuNs.Store.create({visible: false}); - const onAllReadItemClick = (e: React.MouseEvent) => { - e.stopPropagation(); - SetIssuesReadingTimestamp(props.store.issueIds); - IssuesListBoardStore.PageStoreLoadData(props.store); - }; - return ( - <> - - - - - - {list} - - ); -}); \ No newline at end of file + const data = props.store.data; + if (!props.store.loaded || !data) { + return
Loading...
; + } + const list: any[] = []; + for (let i = 0; i < data.length; i++) { + const boardData = data[i]; + const key = boardData.metainfo.title; + const board = ( + + ); + list.push(board); + } + const topRightMenuStore = TopRightMenuNs.Store.create({ visible: false }); + const onAllReadItemClick = (e: React.MouseEvent) => { + e.stopPropagation(); + SetIssuesReadingTimestamp(props.store.issueIds); + IssuesListBoardStore.PageStoreLoadData(props.store); + }; + return ( + <> + + + + + + {list} + + ); +}); diff --git a/frontend/src/issues-list-board/issues-list-card.tsx b/frontend/src/issues-list-board/issues-list-card.tsx index 242546d..ea49df0 100644 --- a/frontend/src/issues-list-board/issues-list-card.tsx +++ b/frontend/src/issues-list-board/issues-list-card.tsx @@ -11,44 +11,63 @@ import { SpentHoursToFixed } from '../utils/spent-hours-to-fixed'; import { getStyleObjectFromString } from '../utils/style'; export type Props = { - store: IIssueStore + store: IIssueStore; }; export const defaultPriorityStyleKey = 'priorityStyle'; export const IssuesListCard = observer((props: Props): JSX.Element => { - const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store); - const detailsStore = IssueDetailsDialogNs.Store.create({ - issue: props.store, - visible: false, - unreadedFlagStore: unreadedStore - }); - const priorityStyle = getStyleObjectFromString(props.store[defaultPriorityStyleKey]); - const tagsNewLine = (props.store.styledTags && props.store.styledTags.length > 0) ?
: null; - return ( -
{ e.stopPropagation(); detailsStore.show(); }}> - -
- -
-
- - - - - {SpentHoursToFixed(props.store.total_spent_hours)} / {SpentHoursToFixed(props.store.total_estimated_hours)} - {tagsNewLine} - -
- {props.store.status.name} - {props.store.priority.name} -
-
-
- ); -}); \ No newline at end of file + const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store); + const detailsStore = IssueDetailsDialogNs.Store.create({ + issue: props.store, + visible: false, + unreadedFlagStore: unreadedStore, + }); + const priorityStyle = getStyleObjectFromString( + props.store[defaultPriorityStyleKey], + ); + const tagsNewLine = + props.store.styledTags && props.store.styledTags.length > 0 ?
: null; + return ( +
{ + e.stopPropagation(); + detailsStore.show(); + }} + > + +
+ +
+
+ + + + + + {SpentHoursToFixed(props.store.total_spent_hours)} /{' '} + {SpentHoursToFixed(props.store.total_estimated_hours)} + + {tagsNewLine} + +
+ {props.store.status.name} + + + {props.store.priority.name} + +
+
+
+ ); +}); diff --git a/frontend/src/issues-list-board/store.ts b/frontend/src/issues-list-board/store.ts index 2442ebf..6fbfe73 100644 --- a/frontend/src/issues-list-board/store.ts +++ b/frontend/src/issues-list-board/store.ts @@ -1,86 +1,93 @@ -import { Instance, types } from "mobx-state-tree"; -import { RedmineTypes } from "../redmine-types"; -import axios from "axios"; +import { Instance, types } from 'mobx-state-tree'; +import { RedmineTypes } from '../redmine-types'; +import axios from 'axios'; export const IssueStore = types.frozen(); -export interface IIssueStore extends Instance {} +export type IIssueStore = Instance; export const MetaInfoStore = types.model({ - title: types.string, - url: types.maybe(types.string), - rootIssue: types.maybe(types.model({ - id: 0, - tracker: types.model({ - id: 0, - name: '' - }), - subject: '' - })) + title: types.string, + url: types.maybe(types.string), + rootIssue: types.maybe( + types.model({ + id: 0, + tracker: types.model({ + id: 0, + name: '', + }), + subject: '', + }), + ), }); export const BoardStore = types.model({ - data: types.array(IssueStore), - metainfo: MetaInfoStore + data: types.array(IssueStore), + metainfo: MetaInfoStore, }); -export interface IBoardStore extends Instance {} +export type IBoardStore = Instance; -export const PageStore = types.model({ - loaded: types.boolean, - type: types.string, - name: types.string, - data: types.maybeNull( - types.array(BoardStore) - ) -}).actions((self) => { - return { - setData: (data: any) => { - self.data = data; - self.loaded = true; - } - }; -}).views((self) => { - return { - get issueIds(): number[] { - if (!self.data) return []; - const data = self.data; - const res = [] as number[]; - for (let i = 0; i < data.length; i++) { - const itemData = data[i]; - for (let j = 0; j < itemData.data.length; j++) { - const issue = itemData.data[j]; - if (res.indexOf(issue.id) < 0) { - res.push(issue.id); - } - } - } - return res; - } - }; -}); +export const PageStore = types + .model({ + loaded: types.boolean, + type: types.string, + name: types.string, + data: types.maybeNull(types.array(BoardStore)), + }) + .actions((self) => { + return { + setData: (data: any) => { + self.data = data; + self.loaded = true; + }, + }; + }) + .views((self) => { + return { + get issueIds(): number[] { + if (!self.data) return []; + const data = self.data; + const res = [] as number[]; + for (let i = 0; i < data.length; i++) { + const itemData = data[i]; + for (let j = 0; j < itemData.data.length; j++) { + const issue = itemData.data[j]; + if (res.indexOf(issue.id) < 0) { + res.push(issue.id); + } + } + } + return res; + }, + }; + }); export async function PageStoreLoadData(store: IPageStore): Promise { - const url = `${process.env.REACT_APP_BACKEND}simple-kanban-board/${store.type}/${store.name}/raw`; - const resp = await axios.get(url); - if (!(resp?.data)) return; - - const data = []; - for (let i = 0; i < resp.data.length; i++) { - const item = resp.data[i] as {data: any[], metainfo: Record}; - data.push({ - metainfo: item.metainfo, - data: item.data ? item.data.map((group: { status: string, count: number, issues: any[] }) => { - return group.issues - }).flat() : [] - }); - } - - /* DEBUG: begin */ - console.debug(`Issues list board store data: ${JSON.stringify(data)}`); - /* DEBUG: end */ - - store.setData(data); + const url = `${process.env.REACT_APP_BACKEND}simple-kanban-board/${store.type}/${store.name}/raw`; + const resp = await axios.get(url); + if (!resp?.data) return; + + const data = []; + for (let i = 0; i < resp.data.length; i++) { + const item = resp.data[i] as { data: any[]; metainfo: Record }; + data.push({ + metainfo: item.metainfo, + data: item.data + ? item.data + .map((group: { status: string; count: number; issues: any[] }) => { + return group.issues; + }) + .flat() + : [], + }); + } + + /* DEBUG: begin */ + console.debug(`Issues list board store data: ${JSON.stringify(data)}`); + /* DEBUG: end */ + + store.setData(data); } -export interface IPageStore extends Instance {} \ No newline at end of file +export type IPageStore = Instance; diff --git a/frontend/src/kanban-board/column.tsx b/frontend/src/kanban-board/column.tsx index 0694ca8..3c70614 100644 --- a/frontend/src/kanban-board/column.tsx +++ b/frontend/src/kanban-board/column.tsx @@ -5,23 +5,26 @@ import { observer } from 'mobx-react-lite'; import * as KanbanCard from './kanban-card'; export type Props = { - store: Stores.IColumnStore -} + store: Stores.IColumnStore; +}; export const Column = observer((props: Props) => { - const cards = props.store.cards.map((card) => { - return ( - - ); - }); - return ( -
-
- {props.store.status} ({props.store.count}) -
- {cards} -
- ); + const cards = props.store.cards.map((card) => { + return ( + + ); + }); + return ( +
+
+ {props.store.status} ({props.store.count}) +
+ {cards} +
+ ); }); export default Column; diff --git a/frontend/src/kanban-board/kanban-board.tsx b/frontend/src/kanban-board/kanban-board.tsx index e90c13c..c96ef6e 100644 --- a/frontend/src/kanban-board/kanban-board.tsx +++ b/frontend/src/kanban-board/kanban-board.tsx @@ -5,29 +5,29 @@ import { observer } from 'mobx-react-lite'; import Column from './column'; export type Props = { - store: IBoardStore + store: IBoardStore; }; export const KanbanBoard = observer((props: Props) => { - let title: any; - if (props.store.metainfo.url) { - title = {props.store.metainfo.title}; - } else { - title = <>{props.store.metainfo.title}; - } - const columns = []; - for (let i = 0; i < props.store.data.length; i++) { - const column = props.store.data[i]; - columns.push() - } - return ( - <> -

{title} #

-
- {columns} -
- - ); + let title: any; + if (props.store.metainfo.url) { + title = {props.store.metainfo.title}; + } else { + title = <>{props.store.metainfo.title}; + } + const columns = []; + for (let i = 0; i < props.store.data.length; i++) { + const column = props.store.data[i]; + columns.push(); + } + return ( + <> +

+ {title} # +

+
{columns}
+ + ); }); -export default KanbanBoard; \ No newline at end of file +export default KanbanBoard; diff --git a/frontend/src/kanban-board/kanban-boards.tsx b/frontend/src/kanban-board/kanban-boards.tsx index b019b0f..acb1898 100644 --- a/frontend/src/kanban-board/kanban-boards.tsx +++ b/frontend/src/kanban-board/kanban-boards.tsx @@ -8,47 +8,51 @@ import axios from 'axios'; import * as ServiceActionsButtons from '../utils/service-actions-buttons'; export type Props = { - store: IPageStore -} + store: IPageStore; +}; export const KanbanBoards = observer((props: Props) => { - const data = props.store.data; - if (!props.store.loaded || !data) { - return
Loading...
- } - const list: any[] = []; - for (let i = 0; i < data.length; i++) { - const boardData = data[i]; - const key = boardData.metainfo.title; - const board = ; - list.push(board); - } - const topRightMenuStore = TopRightMenuNs.Store.create({visible: false}); - const onAllReadClick = (e: React.MouseEvent) => { - e.stopPropagation(); - SetIssuesReadingTimestamp(props.store.issueIds); - PageStoreLoadData(props.store); - }; - let treeRefreshMenuItem: JSX.Element = <>; - if (props.store.canTreeRefresh) { - const onTreeRefreshClick = (e: React.MouseEvent) => { - if (e.target !== e.currentTarget) return; - e.stopPropagation(); - axios.get(`${process.env.REACT_APP_BACKEND}simple-kanban-board/tree/${props.store.name}/refresh`); - } - treeRefreshMenuItem = ; - } - return ( - <> - - - {treeRefreshMenuItem} - - - - {list} - - ); + const data = props.store.data; + if (!props.store.loaded || !data) { + return
Loading...
; + } + const list: any[] = []; + for (let i = 0; i < data.length; i++) { + const boardData = data[i]; + const key = boardData.metainfo.title; + const board = ; + list.push(board); + } + const topRightMenuStore = TopRightMenuNs.Store.create({ visible: false }); + const onAllReadClick = (e: React.MouseEvent) => { + e.stopPropagation(); + SetIssuesReadingTimestamp(props.store.issueIds); + PageStoreLoadData(props.store); + }; + let treeRefreshMenuItem: JSX.Element = <>; + if (props.store.canTreeRefresh) { + const onTreeRefreshClick = (e: React.MouseEvent) => { + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + axios.get( + `${process.env.REACT_APP_BACKEND}simple-kanban-board/tree/${props.store.name}/refresh`, + ); + }; + treeRefreshMenuItem = ( + + ); + } + return ( + <> + + + {treeRefreshMenuItem} + + + + {list} + + ); }); -export default KanbanBoards; \ No newline at end of file +export default KanbanBoards; diff --git a/frontend/src/kanban-board/kanban-card.tsx b/frontend/src/kanban-board/kanban-card.tsx index 16f7b0e..113c015 100644 --- a/frontend/src/kanban-board/kanban-card.tsx +++ b/frontend/src/kanban-board/kanban-card.tsx @@ -9,70 +9,84 @@ import * as IssueDetailsDialogNs from '../misc-components/issue-details-dialog'; import * as UnreadedFlagNs from '../misc-components/unreaded-flag'; export type Props = { - store: ICardStore + store: ICardStore; }; export type TagProps = { - style?: string; - tag: string; + style?: string; + tag: string; }; export const KanbanCardTag = (props: TagProps): JSX.Element => { - const inlineStyle = getStyleObjectFromString(props.style || ''); - return ( - - {props.tag} - - ); -} + const inlineStyle = getStyleObjectFromString(props.style || ''); + return ( + + {props.tag} + + ); +}; /** * Какие дальше требования к карточкам? - * + * * 1. Отобразить как было в статичной доске * 2. Переделать отображение с учётом store.params */ export const KanbanCard = observer((props: Props) => { - let tagsSection = <>; - const tagsParams = props.store.params.fields.find((field) => { - return field.component === 'tags'; - }); - console.debug('Tag params:', tagsParams); // DEBUG - console.debug('Issue:', props.store.issue); // DEBUG - if (tagsParams && props.store.issue[tagsParams.path]) { - const tags = props.store.issue[tagsParams.path] as TagProps[]; - console.debug(`Tags:`, tags); // DEBUG - tagsSection = - } - const timePassedParams: TimePassedNs.Params = { - fromIssue: { - issue: props.store.issue, - keyName: 'timePassedClass' - } - } - const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage(props.store.issue); - const detailsStore = IssueDetailsDialogNs.Store.create({ - issue: props.store.issue, - visible: false, - unreadedFlagStore: unreadedStore - }); - return ( -
{e.stopPropagation(); detailsStore.show();}}> - - -
Исп.: {props.store.issue.current_user.name}
-
Прио.: {props.store.issue.priority.name}
-
Версия: {props.store.issue.fixed_version?.name || ''}
-
Прогресс: {props.store.issue.done_ratio}
-
Трудозатраты: {props.store.issue.total_spent_hours} / {props.store.issue.total_estimated_hours}
- {tagsSection} -
- ); + let tagsSection = <>; + const tagsParams = props.store.params.fields.find((field) => { + return field.component === 'tags'; + }); + console.debug('Tag params:', tagsParams); // DEBUG + console.debug('Issue:', props.store.issue); // DEBUG + if (tagsParams && props.store.issue[tagsParams.path]) { + const tags = props.store.issue[tagsParams.path] as TagProps[]; + console.debug(`Tags:`, tags); // DEBUG + tagsSection = ; + } + const timePassedParams: TimePassedNs.Params = { + fromIssue: { + issue: props.store.issue, + keyName: 'timePassedClass', + }, + }; + const unreadedStore = UnreadedFlagNs.CreateStoreFromLocalStorage( + props.store.issue, + ); + const detailsStore = IssueDetailsDialogNs.Store.create({ + issue: props.store.issue, + visible: false, + unreadedFlagStore: unreadedStore, + }); + return ( +
{ + e.stopPropagation(); + detailsStore.show(); + }} + > + + +
Исп.: {props.store.issue.current_user.name}
+
Прио.: {props.store.issue.priority.name}
+
Версия: {props.store.issue.fixed_version?.name || ''}
+
Прогресс: {props.store.issue.done_ratio}
+
+ Трудозатраты: {props.store.issue.total_spent_hours} /{' '} + {props.store.issue.total_estimated_hours} +
+ {tagsSection} +
+ ); }); -export default KanbanCard; \ No newline at end of file +export default KanbanCard; diff --git a/frontend/src/kanban-board/store.ts b/frontend/src/kanban-board/store.ts index 1a16812..cfaeaf4 100644 --- a/frontend/src/kanban-board/store.ts +++ b/frontend/src/kanban-board/store.ts @@ -2,126 +2,128 @@ import { Instance, types } from 'mobx-state-tree'; import { RedmineTypes } from '../redmine-types'; import axios from 'axios'; -export const IssueStore = types.frozen() +export const IssueStore = types.frozen(); -export interface IIssueStore extends Instance {} +export type IIssueStore = Instance; -export const ColumnStore = types.model({ - status: '', - count: 0, - issues: types.array(IssueStore) -}).views((self) => { - return { - get cards(): ICardStore[] { - return self.issues.map(issue => { - return CardStore.create({ - issue: issue - }) - }); - } - } -}); +export const ColumnStore = types + .model({ + status: '', + count: 0, + issues: types.array(IssueStore), + }) + .views((self) => { + return { + get cards(): ICardStore[] { + return self.issues.map((issue) => { + return CardStore.create({ + issue: issue, + }); + }); + }, + }; + }); -export interface IColumnStore extends Instance {} +export type IColumnStore = Instance; export const MetaInfoStore = types.model({ - title: '', - url: types.maybe(types.string), - rootIssue: types.maybe(types.model({ - id: 0, - tracker: types.model({ - id: 0, - name: '' - }), - subject: '' - })) + title: '', + url: types.maybe(types.string), + rootIssue: types.maybe( + types.model({ + id: 0, + tracker: types.model({ + id: 0, + name: '', + }), + subject: '', + }), + ), }); -export interface IMetaInfoStore extends Instance {} +export type IMetaInfoStore = Instance; export const BoardStore = types.model({ - data: types.array(ColumnStore), - metainfo: MetaInfoStore + data: types.array(ColumnStore), + metainfo: MetaInfoStore, }); -export interface IBoardStore extends Instance {} +export type IBoardStore = Instance; -export const PageStore = types.model({ - loaded: false, - type: '', - name: '', - data: types.maybeNull( - types.array(BoardStore) - ) -}).actions(self => { - return { - setData: (data: any) => { - self.data = data; - self.loaded = true; - } - }; -}).views((self) => { - return { - get issueIds(): number[] { - if (!self.data) return []; - const res = [] as number[]; - for (let i = 0; i < self.data.length; i++) { - const iData = self.data[i]; - for (let j = 0; j < iData.data.length; j++) { - const jData = iData.data[j]; - for (let k = 0; k < jData.issues.length; k++) { - const issue = jData.issues[k]; - if (res.indexOf(issue.id) < 0) { - res.push(issue.id); - } - } - } - } - return res; - }, - get canTreeRefresh(): boolean { - return (self.type === 'tree'); - } - }; -}); +export const PageStore = types + .model({ + loaded: false, + type: '', + name: '', + data: types.maybeNull(types.array(BoardStore)), + }) + .actions((self) => { + return { + setData: (data: any) => { + self.data = data; + self.loaded = true; + }, + }; + }) + .views((self) => { + return { + get issueIds(): number[] { + if (!self.data) return []; + const res = [] as number[]; + for (let i = 0; i < self.data.length; i++) { + const iData = self.data[i]; + for (let j = 0; j < iData.data.length; j++) { + const jData = iData.data[j]; + for (let k = 0; k < jData.issues.length; k++) { + const issue = jData.issues[k]; + if (res.indexOf(issue.id) < 0) { + res.push(issue.id); + } + } + } + } + return res; + }, + get canTreeRefresh(): boolean { + return self.type === 'tree'; + }, + }; + }); export async function PageStoreLoadData(store: IPageStore): Promise { - const url = `${process.env.REACT_APP_BACKEND}simple-kanban-board/${store.type}/${store.name}/raw`; - const resp = await axios.get(url); - if (!(resp?.data)) return; - store.setData(resp.data); + const url = `${process.env.REACT_APP_BACKEND}simple-kanban-board/${store.type}/${store.name}/raw`; + const resp = await axios.get(url); + if (!resp?.data) return; + store.setData(resp.data); } -export interface IPageStore extends Instance { } +export type IPageStore = Instance; export type CardField = { - component: string; + component: string; } & Record; export const CardParamsStore = types.optional( - types.model({ - fields: types.array( - types.frozen() - ), - autoCollapse: types.boolean - }), - { - fields: [ - { component: 'text', label: 'Исп.', path: 'current_user.name' }, - { component: 'text', label: 'Прио.', path: 'priority.name' }, - { component: 'text', label: 'Версия', path: 'fixed_version.name' }, - { component: 'text', label: 'Прогресс', path: 'done_ratio' }, - { component: 'labor_costs' }, - { component: 'tags', label: 'Tags', path: 'styledTags' } - ], - autoCollapse: false, - } + types.model({ + fields: types.array(types.frozen()), + autoCollapse: types.boolean, + }), + { + fields: [ + { component: 'text', label: 'Исп.', path: 'current_user.name' }, + { component: 'text', label: 'Прио.', path: 'priority.name' }, + { component: 'text', label: 'Версия', path: 'fixed_version.name' }, + { component: 'text', label: 'Прогресс', path: 'done_ratio' }, + { component: 'labor_costs' }, + { component: 'tags', label: 'Tags', path: 'styledTags' }, + ], + autoCollapse: false, + }, ); export const CardStore = types.model({ - issue: IssueStore, - params: CardParamsStore + issue: IssueStore, + params: CardParamsStore, }); -export interface ICardStore extends Instance {} - +export type ICardStore = Instance; diff --git a/frontend/src/misc-components/issue-details-dialog.tsx b/frontend/src/misc-components/issue-details-dialog.tsx index 3a9e841..f8958ec 100644 --- a/frontend/src/misc-components/issue-details-dialog.tsx +++ b/frontend/src/misc-components/issue-details-dialog.tsx @@ -9,107 +9,118 @@ import { SetIssueReadingTimestamp } from '../utils/unreaded-provider'; import axios from 'axios'; import * as Luxon from 'luxon'; -export const Store = types.model({ - visible: types.boolean, - issue: types.frozen(), - unreadedFlagStore: types.maybe(UnreadedFlagNs.Store) -}).actions((self) => { - return { - hide: () => { - console.debug(`Issue details dialog hide: issue_id=${self.issue.id}`); // DEBUG - self.visible = false; - }, - show: () => { - console.debug(`Issue details dialog show: issue_id=${self.issue.id}`); // DEBUG - self.visible = true; - if (self.unreadedFlagStore) { - self.unreadedFlagStore.read(); - } else { - SetIssueReadingTimestamp(self.issue.id); - } - } - }; -}).views((self) => { - return { - get displayStyle(): React.CSSProperties { - return {display: self.visible ? 'block' : 'none'}; - } - }; -}); +export const Store = types + .model({ + visible: types.boolean, + issue: types.frozen(), + unreadedFlagStore: types.maybe(UnreadedFlagNs.Store), + }) + .actions((self) => { + return { + hide: () => { + console.debug(`Issue details dialog hide: issue_id=${self.issue.id}`); // DEBUG + self.visible = false; + }, + show: () => { + console.debug(`Issue details dialog show: issue_id=${self.issue.id}`); // DEBUG + self.visible = true; + if (self.unreadedFlagStore) { + self.unreadedFlagStore.read(); + } else { + SetIssueReadingTimestamp(self.issue.id); + } + }, + }; + }) + .views((self) => { + return { + get displayStyle(): React.CSSProperties { + return { display: self.visible ? 'block' : 'none' }; + }, + }; + }); export type Props = { - store: Instance + store: Instance; }; export const IssueDetailsDialog = observer((props: Props): JSX.Element => { - const onUpdateClick = (e: React.MouseEvent) => { - const url = `${process.env.REACT_APP_BACKEND}redmine-event-emitter/append-issues`; - axios.post(url, [props.store.issue.id]); - }; - const onCloseClick = (e: React.MouseEvent) => { - if (e.target !== e.currentTarget) return; - e.stopPropagation(); - props.store.hide(); - }; - return ( -
-
-
-

- - - -

-
-
-

Описание:

-
-							{props.store.issue.description}
-						
-
-
-
-

Комментарии:

- -
-
-
-
- ); + const onUpdateClick = (e: React.MouseEvent) => { + const url = `${process.env.REACT_APP_BACKEND}redmine-event-emitter/append-issues`; + axios.post(url, [props.store.issue.id]); + }; + const onCloseClick = (e: React.MouseEvent) => { + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + props.store.hide(); + }; + return ( +
+
+
+

+ + + +

+
+
+

Описание:

+
{props.store.issue.description}
+
+
+
+

Комментарии:

+ +
+
+
+
+ ); }); -export const Comments = (props: {details?: RedmineTypes.Journal[], issue: RedmineTypes.ExtendedIssue}): JSX.Element => { - const comments = props.details?.filter((detail) => { - return Boolean(detail.notes); - }); - if (!comments) { - return <>No comments - } - const list = comments.map((detail) => { - const key = `issueid_${props.issue.id}_commentid_${detail.id}`; - return - }); - return ( - <>{list} - ); -} +export const Comments = (props: { + details?: RedmineTypes.Journal[]; + issue: RedmineTypes.ExtendedIssue; +}): JSX.Element => { + const comments = props.details?.filter((detail) => { + return Boolean(detail.notes); + }); + if (!comments) { + return <>No comments; + } + const list = comments.map((detail) => { + const key = `issueid_${props.issue.id}_commentid_${detail.id}`; + return ; + }); + return <>{list}; +}; -export const Comment = (props: {data: RedmineTypes.Journal}): JSX.Element => { - const date = Luxon.DateTime.fromISO(props.data.created_on).toFormat("dd.MM.yyyy HH:mm"); - return ( - <> -

{date} {props.data.user.name}:

-
-
-					{props.data.notes || '-'}
-				
-
-
- - ); -} \ No newline at end of file +export const Comment = (props: { data: RedmineTypes.Journal }): JSX.Element => { + const date = Luxon.DateTime.fromISO(props.data.created_on).toFormat( + 'dd.MM.yyyy HH:mm', + ); + return ( + <> +

+ {date} {props.data.user.name}: +

+
+
{props.data.notes || '-'}
+
+
+ + ); +}; diff --git a/frontend/src/misc-components/issue-href.tsx b/frontend/src/misc-components/issue-href.tsx index 6d9aad9..aab7cdc 100644 --- a/frontend/src/misc-components/issue-href.tsx +++ b/frontend/src/misc-components/issue-href.tsx @@ -1,14 +1,16 @@ import React from 'react'; export type Props = { - url: string; - id: number; - subject: string; - tracker: string; + url: string; + id: number; + subject: string; + tracker: string; }; export const IssueHref = (props: Props): JSX.Element => { - return ( - {props.tracker} #{props.id} - {props.subject} - ); -}; \ No newline at end of file + return ( + + {props.tracker} #{props.id} - {props.subject} + + ); +}; diff --git a/frontend/src/misc-components/tag.tsx b/frontend/src/misc-components/tag.tsx index 452dff9..2fd3d1a 100644 --- a/frontend/src/misc-components/tag.tsx +++ b/frontend/src/misc-components/tag.tsx @@ -3,18 +3,18 @@ import { getStyleObjectFromString } from '../utils/style'; import Css from './tag.module.css'; export type Props = { - style?: string; - tag: string; + style?: string; + tag: string; }; export const Tag = (props: Props): JSX.Element => { - const inlineStyle = getStyleObjectFromString(props.style || ''); - return ( - <> - - - {props.tag} - - - ); -} \ No newline at end of file + const inlineStyle = getStyleObjectFromString(props.style || ''); + return ( + <> + + + {props.tag} + + + ); +}; diff --git a/frontend/src/misc-components/tags.tsx b/frontend/src/misc-components/tags.tsx index ae749ab..82aeaa2 100644 --- a/frontend/src/misc-components/tags.tsx +++ b/frontend/src/misc-components/tags.tsx @@ -2,26 +2,28 @@ import React from 'react'; import * as TagNs from './tag'; export type Params = { - label?: string; - tags: TagNs.Props[]; + label?: string; + tags: TagNs.Props[]; }; export type Props = { - params: Params + params: Params; }; export const Tags = (props: Props): JSX.Element => { - if (!props.params.tags) { - return (<>); - } - let label = props.params.label || ''; - if (label) label = `${label}: `; - const tags = props.params.tags.map((tag) => { - return ; - }) || []; - return ( - <> - {label}{tags} - - ); -} \ No newline at end of file + if (!props.params.tags) { + return <>; + } + let label = props.params.label || ''; + if (label) label = `${label}: `; + const tags = + props.params.tags.map((tag) => { + return ; + }) || []; + return ( + <> + {label} + {tags} + + ); +}; diff --git a/frontend/src/misc-components/time-passed.tsx b/frontend/src/misc-components/time-passed.tsx index 0562b2c..b7731d6 100644 --- a/frontend/src/misc-components/time-passed.tsx +++ b/frontend/src/misc-components/time-passed.tsx @@ -3,46 +3,48 @@ import Css from './time-passed.module.css'; import { RedmineTypes } from '../redmine-types'; export type Params = { - fromIssue?: { - issue: RedmineTypes.ExtendedIssue, - keyName: string, - }, - fromValue?: string + fromIssue?: { + issue: RedmineTypes.ExtendedIssue; + keyName: string; + }; + fromValue?: string; }; export type Props = { - params: Params + params: Params; }; export const TimePassed = (props: Props): JSX.Element => { - if (!props.params.fromIssue && !props.params.fromValue) { - return <>; - } - let timePassedClassName = ''; // TODO - if (props.params.fromIssue) { - const { issue, keyName } = props.params.fromIssue; - timePassedClassName = `${Css.timepassedDot} ${getClassName(issue[keyName])}`; - } else if (props.params.fromValue) { - timePassedClassName = `${Css.timepassedDot} ${getClassName(props.params.fromValue)}`; - } - return ( - - ); + if (!props.params.fromIssue && !props.params.fromValue) { + return <>; + } + let timePassedClassName = ''; // TODO + if (props.params.fromIssue) { + const { issue, keyName } = props.params.fromIssue; + timePassedClassName = `${Css.timepassedDot} ${getClassName( + issue[keyName], + )}`; + } else if (props.params.fromValue) { + timePassedClassName = `${Css.timepassedDot} ${getClassName( + props.params.fromValue, + )}`; + } + return ; }; function getClassName(value: string): string { - switch (value) { - case 'hot': - return Css.hot; - case 'warm': - return Css.warm; - case 'comfort': - return Css.comfort; - case 'breezy': - return Css.breezy; - case 'cold': - return Css.cold; - default: - return ''; - } -} \ No newline at end of file + switch (value) { + case 'hot': + return Css.hot; + case 'warm': + return Css.warm; + case 'comfort': + return Css.comfort; + case 'breezy': + return Css.breezy; + case 'cold': + return Css.cold; + default: + return ''; + } +} diff --git a/frontend/src/misc-components/top-right-menu.tsx b/frontend/src/misc-components/top-right-menu.tsx index 5bdc5d7..7e19926 100644 --- a/frontend/src/misc-components/top-right-menu.tsx +++ b/frontend/src/misc-components/top-right-menu.tsx @@ -3,53 +3,62 @@ import { Instance, types } from 'mobx-state-tree'; import React from 'react'; import Css from './top-right-menu.module.css'; -export const Store = types.model({ - visible: types.boolean -}).views((self) => { - return { - get style(): React.CSSProperties { - return { - display: self.visible ? 'block' : 'none' - }; - } - }; -}).actions((self) => { - return { - show: () => { - self.visible = true; - }, - hide: () => { - self.visible = false; - }, - toggle: () => { - self.visible = !self.visible; - } - }; -}); +export const Store = types + .model({ + visible: types.boolean, + }) + .views((self) => { + return { + get style(): React.CSSProperties { + return { + display: self.visible ? 'block' : 'none', + }; + }, + }; + }) + .actions((self) => { + return { + show: () => { + self.visible = true; + }, + hide: () => { + self.visible = false; + }, + toggle: () => { + self.visible = !self.visible; + }, + }; + }); export type Props = { - store: Instance; - children?: any; + store: Instance; + children?: any; }; export const TopRightMenu = observer((props: Props): JSX.Element => { - const menuItems = []; - if (props.children.length > 1) { - for (let key = 0; key < props.children.length; key++) { - const item = props.children[key]; - menuItems.push(
  • {item}
  • ); - } - } else if (props.children) { - menuItems.push(
  • {props.children}
  • ) - } - return ( - <> - -
    -
      - {menuItems} -
    -
    - - ); -}) \ No newline at end of file + const menuItems = []; + if (props.children.length > 1) { + for (let key = 0; key < props.children.length; key++) { + const item = props.children[key]; + menuItems.push(
  • {item}
  • ); + } + } else if (props.children) { + menuItems.push(
  • {props.children}
  • ); + } + return ( + <> + +
    +
      {menuItems}
    +
    + + ); +}); diff --git a/frontend/src/misc-components/unreaded-flag.tsx b/frontend/src/misc-components/unreaded-flag.tsx index f87da53..81432ba 100644 --- a/frontend/src/misc-components/unreaded-flag.tsx +++ b/frontend/src/misc-components/unreaded-flag.tsx @@ -3,62 +3,76 @@ import Css from './unreaded-flag.module.css'; import { observer } from 'mobx-react-lite'; import { Instance, types } from 'mobx-state-tree'; import { RedmineTypes } from '../redmine-types'; -import { GetIssueReadingTimestamp, SetIssueReadingTimestamp } from '../utils/unreaded-provider'; +import { + GetIssueReadingTimestamp, + SetIssueReadingTimestamp, +} from '../utils/unreaded-provider'; -export const Store = types.model({ - issue: types.frozen(), - readingTimestamp: types.number -}).actions((self) => { - return { - read: () => { - self.readingTimestamp = SetIssueReadingTimestamp(self.issue.id); - } - }; -}).views((self) => { - return { - getUpdatedTimestap(): number { - if (self.issue.journals) { - let lastComment: RedmineTypes.Journal | undefined; - for (let i = self.issue.journals.length - 1; i >= 0; i--) { - const journal = self.issue.journals[i]; - if (journal.notes) { - lastComment = journal; - break; - } - } - if (lastComment) { - return (new Date(lastComment.created_on)).getTime(); - } - } - return 0; - }, - getClassName(): string { - let className = Css.circle; - const updatedTimestamp = this.getUpdatedTimestap(); - if (self.readingTimestamp < updatedTimestamp) { - className += ` ${Css.unreaded}`; - } - console.debug(`Unreaded flag getClassName: issueId=${self.issue.id}; readingTimestamp=${self.readingTimestamp}; updatedTimestamp=${updatedTimestamp}; className=${className}`); // DEBUG - return className; - } - }; -}); +export const Store = types + .model({ + issue: types.frozen(), + readingTimestamp: types.number, + }) + .actions((self) => { + return { + read: () => { + self.readingTimestamp = SetIssueReadingTimestamp(self.issue.id); + }, + }; + }) + .views((self) => { + return { + getUpdatedTimestap(): number { + if (self.issue.journals) { + let lastComment: RedmineTypes.Journal | undefined; + for (let i = self.issue.journals.length - 1; i >= 0; i--) { + const journal = self.issue.journals[i]; + if (journal.notes) { + lastComment = journal; + break; + } + } + if (lastComment) { + return new Date(lastComment.created_on).getTime(); + } + } + return 0; + }, + getClassName(): string { + let className = Css.circle; + const updatedTimestamp = this.getUpdatedTimestap(); + if (self.readingTimestamp < updatedTimestamp) { + className += ` ${Css.unreaded}`; + } + console.debug( + `Unreaded flag getClassName: issueId=${self.issue.id}; readingTimestamp=${self.readingTimestamp}; updatedTimestamp=${updatedTimestamp}; className=${className}`, + ); // DEBUG + return className; + }, + }; + }); export function CreateStoreFromLocalStorage(issue: RedmineTypes.ExtendedIssue) { - const timestamp = GetIssueReadingTimestamp(issue.id); - return Store.create({ - issue: issue, - readingTimestamp: timestamp - }); + const timestamp = GetIssueReadingTimestamp(issue.id); + return Store.create({ + issue: issue, + readingTimestamp: timestamp, + }); } export type Props = { - store: Instance -} + store: Instance; +}; export const UnreadedFlag = observer((props: Props): JSX.Element => { - const className = props.store.getClassName(); - return ( - {e.stopPropagation(); props.store.read();}}> - ); -}) \ No newline at end of file + const className = props.store.getClassName(); + return ( + { + e.stopPropagation(); + props.store.read(); + }} + > + ); +}); diff --git a/frontend/src/start-page/basement.tsx b/frontend/src/start-page/basement.tsx index 1bf12e4..9a402eb 100644 --- a/frontend/src/start-page/basement.tsx +++ b/frontend/src/start-page/basement.tsx @@ -10,18 +10,29 @@ export type Props = { export const Basement = (props: Props): JSX.Element => { console.debug('routes:', router.routes); // DEBUG - + return (
    @@ -30,8 +41,12 @@ export const Basement = (props: Props): JSX.Element => {

    ОБСУДИТЬ

    - Сharacter - + Сharacter
    diff --git a/frontend/src/start-page/content-block.tsx b/frontend/src/start-page/content-block.tsx index 1a53e54..4fc9e25 100644 --- a/frontend/src/start-page/content-block.tsx +++ b/frontend/src/start-page/content-block.tsx @@ -1,17 +1,17 @@ import React from 'react'; export type Props = { - title: string; - children?: any; + title: string; + children?: any; }; export const ContentBlock = (props: Props) => { - return ( - <> -

    {props.title}

    - {props.children} - - ); + return ( + <> +

    {props.title}

    + {props.children} + + ); }; -export default ContentBlock; \ No newline at end of file +export default ContentBlock; diff --git a/frontend/src/start-page/content.tsx b/frontend/src/start-page/content.tsx index b9c96aa..9dd70a9 100644 --- a/frontend/src/start-page/content.tsx +++ b/frontend/src/start-page/content.tsx @@ -2,15 +2,11 @@ import React from 'react'; import ContentCss from './content.module.css'; export type Props = { - children?: any; + children?: any; }; export const Content = (props: Props) => { - return ( -
    - {props.children} -
    - ); + return
    {props.children}
    ; }; -export default Content; \ No newline at end of file +export default Content; diff --git a/frontend/src/start-page/cover.tsx b/frontend/src/start-page/cover.tsx index ede82cb..ff27564 100644 --- a/frontend/src/start-page/cover.tsx +++ b/frontend/src/start-page/cover.tsx @@ -2,22 +2,26 @@ import React from 'react'; import CoverCss from './cover.module.css'; export type CoverProps = { - telegramBotUrl: string; + telegramBotUrl: string; }; export const Cover = (props: CoverProps) => { - return ( -
    - Сharacter -
    -

    Redmine Issue Event Emitter

    -

    ОБРАБОТКА И АНАЛИЗ ЗАДАЧ ИЗ "REDMINE"

    -

    - ссылка на телеграмм бота -

    -
    -
    - ); + return ( +
    + Сharacter +
    +

    Redmine Issue Event Emitter

    +

    ОБРАБОТКА И АНАЛИЗ ЗАДАЧ ИЗ "REDMINE"

    +

    + ссылка на телеграмм бота +

    +
    +
    + ); }; -export default Cover; \ No newline at end of file +export default Cover; diff --git a/frontend/src/start-page/notification-block.tsx b/frontend/src/start-page/notification-block.tsx index 1956af1..76096d4 100644 --- a/frontend/src/start-page/notification-block.tsx +++ b/frontend/src/start-page/notification-block.tsx @@ -2,26 +2,34 @@ import React from 'react'; import NotificationBlockCss from './notification-block.module.css'; export type Props = { - avatarUrl: string; - taskTitle?: string; - children?: any; + avatarUrl: string; + taskTitle?: string; + children?: any; }; export const NotificationBlock = (props: Props) => { - const taskTitle = props?.taskTitle - ? ({props.taskTitle} ) - : (<>); - return ( -
    - event_emitter_eltex_loc -
    -

    - {taskTitle} - {props.children} -

    -
    -
    - ); + const taskTitle = props?.taskTitle ? ( + + {props.taskTitle}{' '} + + ) : ( + <> + ); + return ( +
    + event_emitter_eltex_loc +
    +

    + {taskTitle} + {props.children} +

    +
    +
    + ); }; -export default NotificationBlock; \ No newline at end of file +export default NotificationBlock; diff --git a/frontend/src/start-page/start-page.tsx b/frontend/src/start-page/start-page.tsx index a054d79..896599b 100644 --- a/frontend/src/start-page/start-page.tsx +++ b/frontend/src/start-page/start-page.tsx @@ -9,95 +9,132 @@ import StartPageCss from './start-page.module.css'; import TopBar from './top-bar'; export const StartPageData = { - contact: 'https://t.me/pavelgnedov', - bot: 'https://t.me/eltex_event_emitter_bot' + contact: 'https://t.me/pavelgnedov', + bot: 'https://t.me/eltex_event_emitter_bot', }; export const StartPage = () => { - return ( -
    - - - - -
      -
    • Уведомления в реальном времени о событиях из задач - изменения статусов, упоминания комментариев
    • -
    • Генерация и управления отчётами о задачах
    • -
    • Под капотом приложение фреймворк
    • -
    -
    - -
      -
    • Последний отчёт для дейли проект ECCM
    • -
    • Дополнительные функции для разработчиков - eccm:/current_issues_eccm - список текущих задач по статусам - выбираютсятолько задачи из актуальных версий в статусах, где нужна какая-то реакцияили возможна работа прямо сейчас
    • -
    • Скриншоты уведомления от бота: - Примеры уведомлений о новых задачах и об изменениях статусов:
    • -
    - - - Реализовать поддержку нового протокола:

    - Стив Джобс изменил статус задачи с Feedback на Closed -
    - - - Добавить поддержку новых моделей:

    + return ( +
    + + + + +
      +
    • + Уведомления в реальном времени о событиях из задач - изменения + статусов, упоминания комментариев +
    • +
    • Генерация и управления отчётами о задачах
    • +
    • Под капотом приложение фреймворк
    • +
    +
    + +
      +
    • Последний отчёт для дейли проект ECCM
    • +
    • + Дополнительные функции для разработчиков eccm:/current_issues_eccm + - список текущих задач по статусам - выбираютсятолько задачи из + актуальных версий в статусах, где нужна какая-то реакцияили + возможна работа прямо сейчас +
    • +
    • + Скриншоты уведомления от бота: Примеры уведомлений о новых задачах + и об изменениях статусов: +
    • +
    - Билл Гейтс создал новую задачу и назначил её на вас - - -

    Простые уведомления о движении задач - и больше ничего лишнего. - Пример уведомления по личному упоминанию в задаче: -

    - - - Сергей Брин:

    + + Реализовать поддержку нового протокола:
    +
    + Стив Джобс изменил статус задачи с Feedback на Closed +
    - @Ларри Пейдж@, у меня есть хорошая идея. Посмотри, пожалуйста, по описанию к этой задаче. -
    - - - Исправление уязвимости

    + + Добавить поддержку новых моделей:
    +
    + Билл Гейтс создал новую задачу и назначил её на вас +
    - Линус Торвальдс завершил разработку по задаче и передал вам на ревью

    +

    + Простые уведомления о движении задач - и больше ничего лишнего. + Пример уведомления по личному упоминанию в задаче: +

    - Кажется получилось поправить проблемку. Глянь мой MR. -
    - -

    Можно задавать коллегам вопросы прямо из комментария задачи, неотрываясь от её содержимого. Уведомление доставится в считанные минуты с ссылкой на задачу и информацией от кого это уведомление.

    -

    Пример запроса моих текущих задач с помощью команды - /current_issues_eccm -

    - - - Бьёрн Страуструп:

    + + Сергей Брин: +
    +
    + @Ларри Пейдж@, у меня есть хорошая идея. Посмотри, пожалуйста, по + описанию к этой задаче. +
    - Re-opened:

    - - Feature #223301: - Дополнить stdlib новыми функциями (прио - P4, версия - C++23)

    - In Progress:

    - - Question #223411: - Выпуск релиза C++23 (прио - P4, версия - C++23) -
    -
    -
    - -
    - ); + + Исправление уязвимости +
    +
    + Линус Торвальдс завершил разработку по задаче и передал вам на ревью +
    +
    + Кажется получилось поправить проблемку. Глянь мой MR. +
    + +

    + Можно задавать коллегам вопросы прямо из комментария задачи, + неотрываясь от её содержимого. Уведомление доставится в считанные + минуты с ссылкой на задачу и информацией от кого это уведомление. +

    +

    + Пример запроса моих текущих задач с помощью команды + + /current_issues_eccm + +

    + + + Бьёрн Страуструп: +
    +
    + Re-opened: +
    +
    + + {' '} + - Feature #223301:{' '} + + Дополнить stdlib новыми функциями (прио - P4, версия - C++23) +
    +
    + In Progress: +
    +
    + + {' '} + - Question #223411: + + Выпуск релиза C++23 (прио - P4, версия - C++23) +
    +
    +
    + +
    + ); }; -export default StartPage; \ No newline at end of file +export default StartPage; diff --git a/frontend/src/start-page/top-bar.tsx b/frontend/src/start-page/top-bar.tsx index 365dcee..83588ca 100644 --- a/frontend/src/start-page/top-bar.tsx +++ b/frontend/src/start-page/top-bar.tsx @@ -3,29 +3,48 @@ import TopBarCss from './top-bar.module.css'; import LogoImg from './event_emitter_eltex_loc-32px.png'; export type TopBarProps = { - contact: string; - children?: any; + contact: string; + children?: any; }; const TopBar = (props: TopBarProps): ReactElement => { - return ( - + ); }; export default TopBar; diff --git a/frontend/src/unknown-page.tsx b/frontend/src/unknown-page.tsx index 325975f..b00db7c 100644 --- a/frontend/src/unknown-page.tsx +++ b/frontend/src/unknown-page.tsx @@ -1,9 +1,7 @@ import React from 'react'; export const UnknownPage = () => { - return ( -

    Unknown page

    - ) + return

    Unknown page

    ; }; -export default UnknownPage; \ No newline at end of file +export default UnknownPage; diff --git a/frontend/src/utils/service-actions-buttons.tsx b/frontend/src/utils/service-actions-buttons.tsx index 16c1706..6f8cadd 100644 --- a/frontend/src/utils/service-actions-buttons.tsx +++ b/frontend/src/utils/service-actions-buttons.tsx @@ -1,14 +1,15 @@ import React from 'react'; -import { onGetIssuesQueueSizeClick, onIssuesRefreshClick } from './service-actions'; +import { + onGetIssuesQueueSizeClick, + onIssuesRefreshClick, +} from './service-actions'; export const IssuesForceRefreshButton = (): JSX.Element => { - return ( - - ); + return ; }; export const GetIssuesQueueSizeButton = (): JSX.Element => { - return ( - - ); + return ( + + ); }; diff --git a/frontend/src/utils/service-actions.ts b/frontend/src/utils/service-actions.ts index 0c2f950..b0298ac 100644 --- a/frontend/src/utils/service-actions.ts +++ b/frontend/src/utils/service-actions.ts @@ -1,21 +1,31 @@ -import axios from "axios"; +import axios from 'axios'; import React from 'react'; export const onIssuesRefreshClick = (e: React.MouseEvent) => { - if (e.target !== e.currentTarget) return; - e.stopPropagation(); - const rawInput = prompt("Force issues refresh (delimiters - space, comma, semicolon or tab)", ""); - if (!rawInput) return; - const list = rawInput.split(/[ ,;\t\n\r]/).map(item => Number(item)).filter(item => (Number.isFinite(item) && item > 0)); - if (!list) return; - axios.post(`/redmine-event-emitter/append-issues`, list); + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + const rawInput = prompt( + 'Force issues refresh (delimiters - space, comma, semicolon or tab)', + '', + ); + if (!rawInput) return; + const list = rawInput + .split(/[ ,;\t\n\r]/) + .map((item) => Number(item)) + .filter((item) => Number.isFinite(item) && item > 0); + if (!list) return; + axios.post(`/redmine-event-emitter/append-issues`, list); }; -export const onGetIssuesQueueSizeClick = async (e: React.MouseEvent): Promise => { - if (e.target !== e.currentTarget) return; - e.stopPropagation(); - const resp = await axios.get(`${process.env.REACT_APP_BACKEND}redmine-event-emitter/get-issues-queue-size`); - console.debug(`resp -`, resp); // DEBUG - if (!resp || typeof resp.data !== 'number') return; - alert(`Issues queue size - ${resp.data}`); +export const onGetIssuesQueueSizeClick = async ( + e: React.MouseEvent, +): Promise => { + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + const resp = await axios.get( + `${process.env.REACT_APP_BACKEND}redmine-event-emitter/get-issues-queue-size`, + ); + console.debug(`resp -`, resp); // DEBUG + if (!resp || typeof resp.data !== 'number') return; + alert(`Issues queue size - ${resp.data}`); }; diff --git a/frontend/src/utils/spent-hours-to-fixed.ts b/frontend/src/utils/spent-hours-to-fixed.ts index a7e8045..0607f52 100644 --- a/frontend/src/utils/spent-hours-to-fixed.ts +++ b/frontend/src/utils/spent-hours-to-fixed.ts @@ -1,11 +1,13 @@ /** * Форматирование чисел для вывода трудозатрат - * @param a - * @returns + * @param a + * @returns */ -export const SpentHoursToFixed = (a: number|string|null|undefined): string => { - if (a === null || typeof a === 'undefined') return '-'; - const res = (typeof a === 'number') ? a : Number(a); - if (!Number.isFinite(res)) return '-'; - return `${parseFloat(res.toFixed(1))}`; -}; \ No newline at end of file +export const SpentHoursToFixed = ( + a: number | string | null | undefined, +): string => { + if (a === null || typeof a === 'undefined') return '-'; + const res = typeof a === 'number' ? a : Number(a); + if (!Number.isFinite(res)) return '-'; + return `${parseFloat(res.toFixed(1))}`; +}; diff --git a/frontend/src/utils/style.ts b/frontend/src/utils/style.ts index 0b675d2..268f4fc 100644 --- a/frontend/src/utils/style.ts +++ b/frontend/src/utils/style.ts @@ -1,24 +1,26 @@ const formatStringToCamelCase = (str: string): string => { - const splitted = str.split("-"); - if (splitted.length === 1) return splitted[0]; - return ( - splitted[0] + - splitted - .slice(1) - .map(word => word[0].toUpperCase() + word.slice(1)) - .join("") - ); + const splitted = str.split('-'); + if (splitted.length === 1) return splitted[0]; + return ( + splitted[0] + + splitted + .slice(1) + .map((word) => word[0].toUpperCase() + word.slice(1)) + .join('') + ); }; -export const getStyleObjectFromString = (str: string): Record => { - const style = {} as Record; - str.split(";").forEach(el => { - const [property, value] = el.split(":"); - if (!property) return; +export const getStyleObjectFromString = ( + str: string, +): Record => { + const style = {} as Record; + str.split(';').forEach((el) => { + const [property, value] = el.split(':'); + if (!property) return; - const formattedProperty = formatStringToCamelCase(property.trim()); - style[formattedProperty] = value.trim(); - }); + const formattedProperty = formatStringToCamelCase(property.trim()); + style[formattedProperty] = value.trim(); + }); - return style; -}; \ No newline at end of file + return style; +}; diff --git a/frontend/src/utils/unreaded-provider.ts b/frontend/src/utils/unreaded-provider.ts index 2bf4299..9adc8b7 100644 --- a/frontend/src/utils/unreaded-provider.ts +++ b/frontend/src/utils/unreaded-provider.ts @@ -1,23 +1,23 @@ export function GetIssueReadingTimestamp(issueId: number): number { - const value = window.localStorage.getItem(getKey(issueId)); - return value ? Number(value) : 0; + const value = window.localStorage.getItem(getKey(issueId)); + return value ? Number(value) : 0; } export function SetIssueReadingTimestamp(issueId: number): number { - const now = (new Date()).getTime(); - window.localStorage.setItem(getKey(issueId), String(now)); - return now; + const now = new Date().getTime(); + window.localStorage.setItem(getKey(issueId), String(now)); + return now; } export function SetIssuesReadingTimestamp(issueIds: number[]): number { - const now = (new Date()).getTime(); - for (let i = 0; i < issueIds.length; i++) { - const issueId = issueIds[i]; - window.localStorage.setItem(getKey(issueId), String(now)); - } - return now; + const now = new Date().getTime(); + for (let i = 0; i < issueIds.length; i++) { + const issueId = issueIds[i]; + window.localStorage.setItem(getKey(issueId), String(now)); + } + return now; } function getKey(issueId: number): string { - return `issue_read_${issueId}`; -} \ No newline at end of file + return `issue_read_${issueId}`; +} From 2687062906433ea662c1cc51a805a99abbe8f125 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Tue, 3 Oct 2023 08:07:16 +0700 Subject: [PATCH 07/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B5=D0=BD=D0=B4=D0=BF=D0=BE=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BF=D1=83=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=BD=D1=8B=D1=85=20=D0=B4=D0=B0=D1=88=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=D0=B4=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/dashboards/dashboards.controller.ts | 18 ++++++++++++++++++ .../src/dashboards/dashboards.service.ts | 15 +++++++++++++++ libs/event-emitter/src/event-emitter.module.ts | 2 ++ 3 files changed, 35 insertions(+) create mode 100644 libs/event-emitter/src/dashboards/dashboards.controller.ts diff --git a/libs/event-emitter/src/dashboards/dashboards.controller.ts b/libs/event-emitter/src/dashboards/dashboards.controller.ts new file mode 100644 index 0000000..84a3e1a --- /dev/null +++ b/libs/event-emitter/src/dashboards/dashboards.controller.ts @@ -0,0 +1,18 @@ +import { Controller, Get } from '@nestjs/common'; +import { DashboardsService } from './dashboards.service'; +import { BadRequestErrorHandler, getOrAppErrorOrThrow } from '../utils/result'; +import { UNLIMITED } from '../consts/consts'; + +@Controller('api/dashboards') +export class DashboardsController { + constructor(private dashboardsService: DashboardsService) {} + + @Get() + async list(): Promise { + const res = await getOrAppErrorOrThrow( + () => this.dashboardsService.publicList(UNLIMITED), + BadRequestErrorHandler, + ); + return res; + } +} diff --git a/libs/event-emitter/src/dashboards/dashboards.service.ts b/libs/event-emitter/src/dashboards/dashboards.service.ts index 997587a..dade61b 100644 --- a/libs/event-emitter/src/dashboards/dashboards.service.ts +++ b/libs/event-emitter/src/dashboards/dashboards.service.ts @@ -71,4 +71,19 @@ export class DashboardsService { await ds.insert(newValue); return; } + + async publicList(limit: number): Promise<{ id: string; title: string }[]> { + const ds = await this.db.getDatasource(); + const data = await ds.find({ + selector: { + 'data.title': { + $exists: true, + }, + }, + fields: ['id', 'data.title'], + limit: limit, + }); + if (!data.docs) throw createAppError('DASHBOARDS_NOT_FOUND'); + return data.docs.map((d) => ({ id: d.id, title: d.data.title })); + } } diff --git a/libs/event-emitter/src/event-emitter.module.ts b/libs/event-emitter/src/event-emitter.module.ts index 0a6d2f5..5aef956 100644 --- a/libs/event-emitter/src/event-emitter.module.ts +++ b/libs/event-emitter/src/event-emitter.module.ts @@ -35,6 +35,7 @@ import { ListIssuesByUsersWidgetDataLoaderService } from './dashboards/widget-da 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'; +import { DashboardsController } from './dashboards/dashboards.controller'; @Module({}) export class EventEmitterModule implements OnModuleInit { @@ -148,6 +149,7 @@ export class EventEmitterModule implements OnModuleInit { IssuesController, CalendarController, DashboardController, + DashboardsController, ], }; } From 6e2fc1de6c902eaa1528a7e5424fcff3761c59cb Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Fri, 6 Oct 2023 08:19:39 +0700 Subject: [PATCH 08/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=B5=D0=B4=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=B0=D1=88=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=D0=B4=D0=BE=D0=B2=20=D0=BD=D0=B0=20=D1=84=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D0=B5=D0=BD=D0=B4=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/dashboard/dashboard-page.tsx | 11 ++++- frontend/src/dashboard/dashboard-store.tsx | 49 ++++++++++++++++++++++ frontend/src/dashboard/dashboard.tsx | 13 ++++++ frontend/src/dashboard/dashboards-list.tsx | 23 ++++++++++ frontend/src/dashboard/dashboards-page.tsx | 8 +++- frontend/src/dashboard/dashboards-store.ts | 32 ++++++++++++++ 6 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 frontend/src/dashboard/dashboard-store.tsx create mode 100644 frontend/src/dashboard/dashboard.tsx create mode 100644 frontend/src/dashboard/dashboards-list.tsx create mode 100644 frontend/src/dashboard/dashboards-store.ts diff --git a/frontend/src/dashboard/dashboard-page.tsx b/frontend/src/dashboard/dashboard-page.tsx index fe362e1..a256449 100644 --- a/frontend/src/dashboard/dashboard-page.tsx +++ b/frontend/src/dashboard/dashboard-page.tsx @@ -1,8 +1,17 @@ import React from 'react'; import { useParams } from 'react-router-dom'; +import * as Store from './dashboard-store'; +import { Dashboard } from './dashboard'; export const DashboardPage = (): JSX.Element => { const params = useParams(); const id = params.id as string; - return

    Dashboard

    ; + + const store = Store.Dashboard.create({ + id: id, + loaded: false, + }); + Store.DashboardLoadData(store); + + return ; }; diff --git a/frontend/src/dashboard/dashboard-store.tsx b/frontend/src/dashboard/dashboard-store.tsx new file mode 100644 index 0000000..fd18fa4 --- /dev/null +++ b/frontend/src/dashboard/dashboard-store.tsx @@ -0,0 +1,49 @@ +import axios from 'axios'; +import { Instance, types } from 'mobx-state-tree'; + +type _WidgetParams = { + collapsed?: boolean; +} & Record; + +export const WidgetParams = types.frozen<_WidgetParams>(); + +type _DataLoaderParams = Record | null; + +export const DataLoaderParams = types.frozen<_DataLoaderParams>(); + +export const Widget = types.model({ + type: types.string, + id: types.string, + title: types.string, + widgetParams: types.maybe(WidgetParams), + dataLoaderParams: types.maybe(DataLoaderParams), +}); + +export const Data = types.model({ + widgets: types.array(Widget), + title: types.maybe(types.string), +}); + +export const Dashboard = types + .model({ + loaded: types.boolean, + id: types.string, + data: types.maybe(Data), + }) + .actions((self) => { + return { + setData: (data: any) => { + self.data = data; + self.loaded = true; + }, + }; + }); + +export type IDashboard = Instance; + +export async function DashboardLoadData(store: IDashboard): Promise { + const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${store.id}`; + const resp = await axios.get(url); + if (!resp.data) return; + store.setData(resp.data); +} diff --git a/frontend/src/dashboard/dashboard.tsx b/frontend/src/dashboard/dashboard.tsx new file mode 100644 index 0000000..7dfd4e2 --- /dev/null +++ b/frontend/src/dashboard/dashboard.tsx @@ -0,0 +1,13 @@ +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import * as Store from './dashboard-store'; + +export type Props = { store: Store.IDashboard }; + +export const Dashboard = observer((props: Props): JSX.Element => { + if (!props.store.loaded) { + return
    Loading... {JSON.stringify(props.store)}
    ; + } + + return
    {JSON.stringify(props.store, null, '  ')}
    ; +}); diff --git a/frontend/src/dashboard/dashboards-list.tsx b/frontend/src/dashboard/dashboards-list.tsx new file mode 100644 index 0000000..8a2bd0b --- /dev/null +++ b/frontend/src/dashboard/dashboards-list.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import * as Store from './dashboards-store'; +import { observer } from 'mobx-react-lite'; + +export type Props = { store: Store.IList }; + +export const DashboardsList = observer((props: Props): JSX.Element => { + if (!props.store.loaded) { + return
    Loading...
    ; + } + + return ( +
      + {props.store.list.map((item) => { + return ( +
    • + {item.title} +
    • + ); + })} +
    + ); +}); diff --git a/frontend/src/dashboard/dashboards-page.tsx b/frontend/src/dashboard/dashboards-page.tsx index 49b0110..d251890 100644 --- a/frontend/src/dashboard/dashboards-page.tsx +++ b/frontend/src/dashboard/dashboards-page.tsx @@ -1,6 +1,10 @@ import React from 'react'; +import { DashboardsList } from './dashboards-list'; +import * as Store from './dashboards-store'; export const DashboardsPage = (): JSX.Element => { - // TODO: code for DashboardsPage - return

    Dashboards

    ; + const store = Store.List.create({ loaded: false, list: [] }); + Store.ListStoreLoadData(store); + + return ; }; diff --git a/frontend/src/dashboard/dashboards-store.ts b/frontend/src/dashboard/dashboards-store.ts new file mode 100644 index 0000000..6c980da --- /dev/null +++ b/frontend/src/dashboard/dashboards-store.ts @@ -0,0 +1,32 @@ +import axios from 'axios'; +import { Instance, types } from 'mobx-state-tree'; + +export const Item = types.model({ + id: types.string, + title: types.string, +}); + +export type IItem = Instance; + +export const List = types + .model({ + list: types.array(Item), + loaded: types.boolean, + }) + .actions((self) => { + return { + setList: (data: any) => { + self.list = data; + self.loaded = true; + }, + }; + }); + +export type IList = Instance; + +export async function ListStoreLoadData(store: IList): Promise { + const url = `${process.env.REACT_APP_BACKEND}api/dashboards`; + const resp = await axios.get(url); + if (!resp?.data) return; + store.setList(resp.data); +} From 05b6364ae54cf4d8f05ee0ef7cf31725eab70873 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Fri, 6 Oct 2023 08:26:55 +0700 Subject: [PATCH 09/15] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=20=D0=B2=D0=B8=D0=B4=D0=B6=D0=B5=D1=82=D0=B0=20=D0=B4=D0=B0?= =?UTF-8?q?=D1=88=D0=B1=D0=BE=D1=80=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/dashboard/dashboard-store.tsx | 5 ++--- libs/event-emitter/src/models/dashboard.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/src/dashboard/dashboard-store.tsx b/frontend/src/dashboard/dashboard-store.tsx index fd18fa4..78cca96 100644 --- a/frontend/src/dashboard/dashboard-store.tsx +++ b/frontend/src/dashboard/dashboard-store.tsx @@ -1,9 +1,7 @@ import axios from 'axios'; import { Instance, types } from 'mobx-state-tree'; -type _WidgetParams = { - collapsed?: boolean; -} & Record; +type _WidgetParams = Record | null; export const WidgetParams = types.frozen<_WidgetParams>(); @@ -15,6 +13,7 @@ export const Widget = types.model({ type: types.string, id: types.string, title: types.string, + collapsed: types.maybe(types.boolean), widgetParams: types.maybe(WidgetParams), dataLoaderParams: types.maybe(DataLoaderParams), }); diff --git a/libs/event-emitter/src/models/dashboard.ts b/libs/event-emitter/src/models/dashboard.ts index 8f28995..88e1889 100644 --- a/libs/event-emitter/src/models/dashboard.ts +++ b/libs/event-emitter/src/models/dashboard.ts @@ -11,9 +11,7 @@ export type Dashboard = { /** * Параметры для отрисовки данных */ -export type WidgetParams = { - collapsed?: boolean; -} & Record; +export type WidgetParams = Record | null; /** * Параметры для загрузки данных @@ -24,6 +22,7 @@ export type Widget = { type: string; id: string; title: string; + collapsed?: boolean; widgetParams?: WidgetParams; dataLoaderParams?: DataLoaderParams; }; From ae14189cd334e405b650ee6c7244d790ae955e71 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Fri, 3 Nov 2023 07:44:36 +0700 Subject: [PATCH 10/15] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=20=D0=BF=D0=B5=D1=80=D0=B2=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=B2=D0=B8=D0=B4=D0=B6=D0=B5=D1=82=20kanban=5Fby=5Ftree=20?= =?UTF-8?q?=D0=BA=20=D0=B4=D0=B0=D1=88=D0=B1=D0=BE=D1=80=D0=B4=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/dashboard/dashboard-page.tsx | 4 +- frontend/src/dashboard/dashboard-store.tsx | 111 ++++++++++++++++-- frontend/src/dashboard/dashboard.tsx | 29 ++++- frontend/src/dashboard/widget.tsx | 96 +++++++++++++++ .../src/dashboard/widgets/kanban-by-tree.tsx | 17 +++ .../src/dashboard/widgets/widget-factory.tsx | 26 ++++ .../src/misc-components/debug-info.module.css | 5 + frontend/src/misc-components/debug-info.tsx | 19 +++ .../src/dashboards/dashboard.controller.ts | 11 ++ .../src/dashboards/dashboards-data.service.ts | 22 +++- 10 files changed, 327 insertions(+), 13 deletions(-) create mode 100644 frontend/src/dashboard/widget.tsx create mode 100644 frontend/src/dashboard/widgets/kanban-by-tree.tsx create mode 100644 frontend/src/dashboard/widgets/widget-factory.tsx create mode 100644 frontend/src/misc-components/debug-info.module.css create mode 100644 frontend/src/misc-components/debug-info.tsx diff --git a/frontend/src/dashboard/dashboard-page.tsx b/frontend/src/dashboard/dashboard-page.tsx index a256449..4e77823 100644 --- a/frontend/src/dashboard/dashboard-page.tsx +++ b/frontend/src/dashboard/dashboard-page.tsx @@ -11,7 +11,9 @@ export const DashboardPage = (): JSX.Element => { id: id, loaded: false, }); - Store.DashboardLoadData(store); + Store.DashboardLoadData(store).then(() => { + return Store.LoadDataForWidgets(store); + }); return ; }; diff --git a/frontend/src/dashboard/dashboard-store.tsx b/frontend/src/dashboard/dashboard-store.tsx index 78cca96..dffcc2d 100644 --- a/frontend/src/dashboard/dashboard-store.tsx +++ b/frontend/src/dashboard/dashboard-store.tsx @@ -9,14 +9,58 @@ type _DataLoaderParams = Record | null; export const DataLoaderParams = types.frozen<_DataLoaderParams>(); -export const Widget = types.model({ - type: types.string, - id: types.string, - title: types.string, - collapsed: types.maybe(types.boolean), - widgetParams: types.maybe(WidgetParams), - dataLoaderParams: types.maybe(DataLoaderParams), -}); +export const Widget = types + .model({ + type: types.string, + id: types.string, + title: types.string, + collapsed: types.maybe(types.boolean), + visible: types.boolean, + widgetParams: types.maybe(WidgetParams), + dataLoaderParams: types.maybe(DataLoaderParams), + loaded: false, + data: types.maybe(types.frozen()), + }) + .actions((self) => { + return { + show: () => { + self.visible = false; + }, + hide: () => { + self.visible = true; + }, + toggle: () => { + self.visible = !self.visible; + }, + setData: (data: any) => { + self.loaded = true; + self.data = data; + }, + }; + }); + +export type IWidget = Instance; + +export function createWidgetStore( + id: string, + type: string, + title: string, + collapsed = false, + widgetParams?: _WidgetParams, + dataLoaderParams?: _DataLoaderParams, +): IWidget { + // eslint-disable-next-line prefer-rest-params + console.debug('init new Widget store, params:', arguments); + return Widget.create({ + id: id, + type: type, + title: title, + collapsed: collapsed, + visible: !collapsed, + widgetParams: widgetParams, + dataLoaderParams: dataLoaderParams, + }); +} export const Data = types.model({ widgets: types.array(Widget), @@ -32,17 +76,68 @@ export const Dashboard = types .actions((self) => { return { setData: (data: any) => { + console.debug('Dashboard store new data:', data); // DEBUG + if (data.widgets) { + for (let i = 0; i < data.widgets.length; i++) { + const widget = data.widgets[i]; + widget.visible = !widget.collapsed; + } + } self.data = data; self.loaded = true; }, + setWidgetsData: (data: any) => { + for (let i = 0; i < data.length; i++) { + const widgetData = data[i]?.data; + const widgetId = data[i]?.widget?.id; + if (!widgetId || !widgetData) continue; + + const widgets = self.data?.widgets; + if (!widgets || widgets.length <= 0) return; + + const widget = widgets.find((w) => w.id == widgetId); + if (!widget) continue; + + widget.setData(widgetData); + } + }, + setWidgetData: (widgetId: string, data: any) => { + const widget = self.data?.widgets.find((w) => w.id == widgetId); + if (!widget) return; + widget.setData(data); + }, }; }); export type IDashboard = Instance; export async function DashboardLoadData(store: IDashboard): Promise { + console.debug('DashboardLoadData store:', store); // DEBUG const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${store.id}`; const resp = await axios.get(url); + console.debug('DashboardLoadData resp:', resp); // DEBUG if (!resp.data) return; store.setData(resp.data); } + +export async function LoadDataForWidgets(store: IDashboard): Promise { + const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${store.id}/load-data`; + const resp = await fetch(url); + if (resp && resp.ok) { + const data = await resp.json(); + store.setWidgetsData(data); + } +} + +export async function LoadDataForWidget( + store: IDashboard, + widgetId: string, +): Promise { + const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${store.id}/load-data/${widgetId}`; + const resp = await fetch(url); + if (resp && resp.ok) { + const data = await resp.json(); + store.setWidgetData(widgetId, data); + } + return; +} diff --git a/frontend/src/dashboard/dashboard.tsx b/frontend/src/dashboard/dashboard.tsx index 7dfd4e2..47ef169 100644 --- a/frontend/src/dashboard/dashboard.tsx +++ b/frontend/src/dashboard/dashboard.tsx @@ -1,13 +1,36 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; -import * as Store from './dashboard-store'; +import * as DashboardStoreNs from './dashboard-store'; +import * as TopRightMenuNs from '../misc-components/top-right-menu'; +import * as WidgetNs from './widget'; +import { DebugInfo } from '../misc-components/debug-info'; -export type Props = { store: Store.IDashboard }; +export type Props = { store: DashboardStoreNs.IDashboard }; export const Dashboard = observer((props: Props): JSX.Element => { if (!props.store.loaded) { + console.debug('Dashboard - store:', JSON.stringify(props.store)); // DEBUG return
    Loading... {JSON.stringify(props.store)}
    ; } - return
    {JSON.stringify(props.store, null, '  ')}
    ; + const debugInfo = JSON.stringify(props.store, null, ' '); + + const topRightMenuStore = TopRightMenuNs.Store.create({ visible: false }); + + const widgets = props.store.data?.widgets.map((widget) => { + return ; + }); + + const res = ( +
    + + Назад + Дашборд - {props.store.data?.title || props.store.id} + + {widgets} + +
    + ); + + return res; }); diff --git a/frontend/src/dashboard/widget.tsx b/frontend/src/dashboard/widget.tsx new file mode 100644 index 0000000..0008e55 --- /dev/null +++ b/frontend/src/dashboard/widget.tsx @@ -0,0 +1,96 @@ +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import * as DashboardStoreNs from './dashboard-store'; +import * as WidgetFactoryNs from './widgets/widget-factory'; + +export type Props = { + store: DashboardStoreNs.IWidget; +}; + +/** + * Пример данных передаваемых в виджет: + * + * { + * "type": "kanban_by_tree", + * "id": "first", + * "title": "Первый виджет", + * "dataLoaderParams": { + * "rootIssueId": 2, + * "groups": { + * "fromIssues": [ + * { + * "issueId": 3, + * "name": "Тест" + * } + * ], + * "fromIssuesIncluded": false, + * "showOthers": true + * }, + * "statuses": [ + * "New", + * "Re-opened", + * "In Progress", + * "Code Review", + * "Resolved", + * "Testing", + * "Feedback", + * "Wait Release", + * "Pending", + * "Closed", + * "Rejected" + * ], + * "tags": { + * "tagsKeyName": "tags", + * "styledTagsKeyName": "styledTags", + * "styles": { + * "supertag": "background-color: rgb(128, 0, 0); color: rgb(255, 255, 255);" + * }, + * "defaultStyle": "background-color: rgb(128, 128, 128);" + * }, + * "priorities": [ + * { + * "rules": [ + * { + * "priorityName": "P1", + * "style": "background-color: #DB2228;" + * }, + * { + * "priorityName": "P2", + * "style": "background-color: #FA5E26;" + * }, + * { + * "priorityName": "P3", + * "style": "background-color: #FDAF19;" + * }, + * { + * "priorityName": "P4", + * "style": "background-color: #31A8FF;", + * "default": true + * }, + * { + * "priorityName": "P5", + * "style": "background-color: #FFFFFF; border: 0.5px solid #393838; color: #202020;" + * } + * ], + * "targetKey": "priorityStyle" + * } + * ] + * } + * } + */ + +export const Widget = observer((props: Props): JSX.Element => { + const display = props.store.visible ? 'block' : 'none'; + + return ( +
    +
    + + Title - {props.store.title} +
    +
    + +
    +
    + ); +}); diff --git a/frontend/src/dashboard/widgets/kanban-by-tree.tsx b/frontend/src/dashboard/widgets/kanban-by-tree.tsx new file mode 100644 index 0000000..8641284 --- /dev/null +++ b/frontend/src/dashboard/widgets/kanban-by-tree.tsx @@ -0,0 +1,17 @@ +import { Instance, onSnapshot } from 'mobx-state-tree'; +import * as DashboardStoreNs from '../dashboard-store'; +import { observer } from 'mobx-react-lite'; +import * as KanbanBoardsNs from '../../kanban-board/kanban-boards'; +import * as KanbanBoardsStoreNs from '../../kanban-board/store'; + +export type Props = { + store: Instance; +}; + +export const KanbanByTree = observer((props: Props): JSX.Element => { + const store = KanbanBoardsStoreNs.PageStore.create({ loaded: false }); + onSnapshot(props.store, (state) => { + store.setData(state.data.data); + }); + return ; +}); diff --git a/frontend/src/dashboard/widgets/widget-factory.tsx b/frontend/src/dashboard/widgets/widget-factory.tsx new file mode 100644 index 0000000..1cb27fb --- /dev/null +++ b/frontend/src/dashboard/widgets/widget-factory.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import * as DashboardStoreNs from '../dashboard-store'; +import { Instance } from 'mobx-state-tree'; +import { observer } from 'mobx-react-lite'; +import * as KanbanByTreeWidgetNs from './kanban-by-tree'; +import { DebugInfo } from '../../misc-components/debug-info'; + +export type Props = { + store: Instance; +}; + +export const WidgetFactory = observer((props: Props): JSX.Element => { + const type = props.store.type; + + switch (type) { + case 'kanban_by_tree': + return ; + default: + return ( +
    +
    Unknown widget
    + +
    + ); + } +}); diff --git a/frontend/src/misc-components/debug-info.module.css b/frontend/src/misc-components/debug-info.module.css new file mode 100644 index 0000000..f429782 --- /dev/null +++ b/frontend/src/misc-components/debug-info.module.css @@ -0,0 +1,5 @@ +.debugInfo { + margin: 3px; + padding: 3px; + border: 1px solid black; +} \ No newline at end of file diff --git a/frontend/src/misc-components/debug-info.tsx b/frontend/src/misc-components/debug-info.tsx new file mode 100644 index 0000000..de71c46 --- /dev/null +++ b/frontend/src/misc-components/debug-info.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import Css from './debug-info.module.css'; + +export type Props = { + value?: string; + children?: any; +}; + +export const DebugInfo = (props: Props): JSX.Element => { + let output: any; + if (props.value) { + output =
    {props.value}
    ; + } else if (props.children) { + output = props.children; + } else { + output =
    (none)
    ; + } + return
    {output}
    ; +}; diff --git a/libs/event-emitter/src/dashboards/dashboard.controller.ts b/libs/event-emitter/src/dashboards/dashboard.controller.ts index 013f769..980c234 100644 --- a/libs/event-emitter/src/dashboards/dashboard.controller.ts +++ b/libs/event-emitter/src/dashboards/dashboard.controller.ts @@ -36,6 +36,17 @@ export class DashboardController { ); } + @Get(':id/load-data/:widgetId') + async loadDataForWidget( + @Param('id') id: string, + @Param('widgetId') widgetId: string, + ): Promise { + return await getOrAppErrorOrThrow( + () => this.dashboardsDataService.loadDataForWidget(id, widgetId), + BadRequestErrorHandler, + ); + } + @Put(':id') async save(@Param('id') id: string, @Body() data: any): Promise { const res = await getOrAppErrorOrThrow( diff --git a/libs/event-emitter/src/dashboards/dashboards-data.service.ts b/libs/event-emitter/src/dashboards/dashboards-data.service.ts index 74fe340..484be8b 100644 --- a/libs/event-emitter/src/dashboards/dashboards-data.service.ts +++ b/libs/event-emitter/src/dashboards/dashboards-data.service.ts @@ -24,6 +24,7 @@ export class DashboardsDataService { let isSuccess = false; for (let i = 0; i < cfg.widgets.length; i++) { const widget = cfg.widgets[i]; + if (widget.collapsed) continue; const loadRes = await this.loadWidgetData( widget.type, widget.widgetParams, @@ -33,13 +34,32 @@ export class DashboardsDataService { if (loadRes.result) { isSuccess = true; loadRes.result.widgetId = widget.id; - results.push(loadRes.result); + results.push({ data: loadRes.result, widget: widget }); } } if (!isSuccess) throw createAppError('CANNOT_LOAD_DATA'); return results; } + async loadDataForWidget( + id: string, + widgetId: string, + ): Promise { + const cfg = await this.dashboardsService.load(id); + const widget = cfg.widgets.find((widget) => { + return widget.id == widgetId; + }); + if (!widget) throw createAppError('WIDGET_NOT_FOUND'); + const loadRes = await this.loadWidgetData( + widget.type, + widget.widgetParams, + widget.dataLoaderParams, + cfg, + ); + if (loadRes.result) return { widget: widget, data: loadRes.error }; + throw createAppError('CANNOT_LOAD_DATA'); + } + async loadWidgetData( type: string, widgetParams: DashboardModel.WidgetParams, From a756677c897529b15f2625da6029c71a019772a9 Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Fri, 3 Nov 2023 20:18:22 +0700 Subject: [PATCH 11/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B2=D1=81=D0=B5=20=D0=B2=D0=B8=D0=B4?= =?UTF-8?q?=D0=B6=D0=B5=D1=82=D1=8B=20=D0=BD=D0=B0=20=D0=B4=D0=B0=D1=88?= =?UTF-8?q?=D0=B1=D0=BE=D1=80=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/dashboard/dashboard-store.tsx | 23 +++++++++++----- frontend/src/dashboard/dashboard.tsx | 5 +--- frontend/src/dashboard/widget.tsx | 11 +++++++- .../src/dashboard/widgets/issues-list.tsx | 18 +++++++++++++ .../{kanban-by-tree.tsx => kanban.tsx} | 4 +-- .../src/dashboard/widgets/widget-factory.tsx | 26 +++++++++++-------- .../issues-list-board/issues-list-card.tsx | 2 +- frontend/src/issues-list-board/store.ts | 6 ++--- frontend/src/kanban-board/store.ts | 1 + frontend/src/utils/style.ts | 1 + .../src/dashboards/dashboards-data.service.ts | 2 +- 11 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 frontend/src/dashboard/widgets/issues-list.tsx rename frontend/src/dashboard/widgets/{kanban-by-tree.tsx => kanban.tsx} (81%) diff --git a/frontend/src/dashboard/dashboard-store.tsx b/frontend/src/dashboard/dashboard-store.tsx index dffcc2d..594b55b 100644 --- a/frontend/src/dashboard/dashboard-store.tsx +++ b/frontend/src/dashboard/dashboard-store.tsx @@ -1,5 +1,5 @@ import axios from 'axios'; -import { Instance, types } from 'mobx-state-tree'; +import { IAnyModelType, Instance, types } from 'mobx-state-tree'; type _WidgetParams = Record | null; @@ -20,6 +20,7 @@ export const Widget = types dataLoaderParams: types.maybe(DataLoaderParams), loaded: false, data: types.maybe(types.frozen()), + dashboardId: types.maybe(types.string), }) .actions((self) => { return { @@ -36,6 +37,9 @@ export const Widget = types self.loaded = true; self.data = data; }, + setDashboardId: (dashboardId: string) => { + self.dashboardId = dashboardId; + }, }; }); @@ -49,8 +53,6 @@ export function createWidgetStore( widgetParams?: _WidgetParams, dataLoaderParams?: _DataLoaderParams, ): IWidget { - // eslint-disable-next-line prefer-rest-params - console.debug('init new Widget store, params:', arguments); return Widget.create({ id: id, type: type, @@ -76,7 +78,6 @@ export const Dashboard = types .actions((self) => { return { setData: (data: any) => { - console.debug('Dashboard store new data:', data); // DEBUG if (data.widgets) { for (let i = 0; i < data.widgets.length; i++) { const widget = data.widgets[i]; @@ -112,10 +113,8 @@ export const Dashboard = types export type IDashboard = Instance; export async function DashboardLoadData(store: IDashboard): Promise { - console.debug('DashboardLoadData store:', store); // DEBUG const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${store.id}`; const resp = await axios.get(url); - console.debug('DashboardLoadData resp:', resp); // DEBUG if (!resp.data) return; store.setData(resp.data); } @@ -141,3 +140,15 @@ export async function LoadDataForWidget( } return; } + +export async function WidgetLoadData(widget: IWidget): Promise { + if (!widget.dashboardId) return; + const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${widget.dashboardId}/load-data/${widget.id}`; + const resp = await fetch(url); + if (resp && resp.ok) { + const data = await resp.json(); + if (data?.data) { + widget.setData(data?.data); + } + } +} diff --git a/frontend/src/dashboard/dashboard.tsx b/frontend/src/dashboard/dashboard.tsx index 47ef169..654e996 100644 --- a/frontend/src/dashboard/dashboard.tsx +++ b/frontend/src/dashboard/dashboard.tsx @@ -3,7 +3,6 @@ import React from 'react'; import * as DashboardStoreNs from './dashboard-store'; import * as TopRightMenuNs from '../misc-components/top-right-menu'; import * as WidgetNs from './widget'; -import { DebugInfo } from '../misc-components/debug-info'; export type Props = { store: DashboardStoreNs.IDashboard }; @@ -13,11 +12,10 @@ export const Dashboard = observer((props: Props): JSX.Element => { return
    Loading... {JSON.stringify(props.store)}
    ; } - const debugInfo = JSON.stringify(props.store, null, ' '); - const topRightMenuStore = TopRightMenuNs.Store.create({ visible: false }); const widgets = props.store.data?.widgets.map((widget) => { + widget.setDashboardId(props.store.id); return ; }); @@ -28,7 +26,6 @@ export const Dashboard = observer((props: Props): JSX.Element => { Дашборд - {props.store.data?.title || props.store.id} {widgets} - ); diff --git a/frontend/src/dashboard/widget.tsx b/frontend/src/dashboard/widget.tsx index 0008e55..214afdb 100644 --- a/frontend/src/dashboard/widget.tsx +++ b/frontend/src/dashboard/widget.tsx @@ -79,13 +79,22 @@ export type Props = { * } */ +function onWidgetVisibleToggleClick(store: DashboardStoreNs.IWidget): void { + if (!store.loaded && store.dashboardId) { + DashboardStoreNs.WidgetLoadData(store); + } + store.toggle(); +} + export const Widget = observer((props: Props): JSX.Element => { const display = props.store.visible ? 'block' : 'none'; return (
    - + Title - {props.store.title}
    diff --git a/frontend/src/dashboard/widgets/issues-list.tsx b/frontend/src/dashboard/widgets/issues-list.tsx new file mode 100644 index 0000000..3cbdcd4 --- /dev/null +++ b/frontend/src/dashboard/widgets/issues-list.tsx @@ -0,0 +1,18 @@ +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import * as DashboardStoreNs from '../dashboard-store'; +import * as IssuesListBoardsNs from '../../issues-list-board/issues-list-boards'; +import * as IssuesListBoardsStoreNs from '../../issues-list-board/store'; +import { onSnapshot } from 'mobx-state-tree'; + +export type Props = { + store: DashboardStoreNs.IWidget; +}; + +export const IssuesList = observer((props: Props): JSX.Element => { + const store = IssuesListBoardsStoreNs.PageStore.create({ loaded: false }); + onSnapshot(props.store, (state) => { + if (state?.data?.data) store.setData(state?.data?.data); + }); + return ; +}); diff --git a/frontend/src/dashboard/widgets/kanban-by-tree.tsx b/frontend/src/dashboard/widgets/kanban.tsx similarity index 81% rename from frontend/src/dashboard/widgets/kanban-by-tree.tsx rename to frontend/src/dashboard/widgets/kanban.tsx index 8641284..d86f8ec 100644 --- a/frontend/src/dashboard/widgets/kanban-by-tree.tsx +++ b/frontend/src/dashboard/widgets/kanban.tsx @@ -8,10 +8,10 @@ export type Props = { store: Instance; }; -export const KanbanByTree = observer((props: Props): JSX.Element => { +export const Kanban = observer((props: Props): JSX.Element => { const store = KanbanBoardsStoreNs.PageStore.create({ loaded: false }); onSnapshot(props.store, (state) => { - store.setData(state.data.data); + if (state?.data?.data) store.setData(state?.data?.data); }); return ; }); diff --git a/frontend/src/dashboard/widgets/widget-factory.tsx b/frontend/src/dashboard/widgets/widget-factory.tsx index 1cb27fb..df2ece7 100644 --- a/frontend/src/dashboard/widgets/widget-factory.tsx +++ b/frontend/src/dashboard/widgets/widget-factory.tsx @@ -2,8 +2,9 @@ import React from 'react'; import * as DashboardStoreNs from '../dashboard-store'; import { Instance } from 'mobx-state-tree'; import { observer } from 'mobx-react-lite'; -import * as KanbanByTreeWidgetNs from './kanban-by-tree'; +import * as KanbanWidgetNs from './kanban'; import { DebugInfo } from '../../misc-components/debug-info'; +import * as IssuesListNs from './issues-list'; export type Props = { store: Instance; @@ -12,15 +13,18 @@ export type Props = { export const WidgetFactory = observer((props: Props): JSX.Element => { const type = props.store.type; - switch (type) { - case 'kanban_by_tree': - return ; - default: - return ( -
    -
    Unknown widget
    - -
    - ); + if (type.startsWith('kanban_by_')) { + return ; } + + if (type.startsWith('issues_list_')) { + return ; + } + + return ( +
    +
    Unknown widget
    + +
    + ); }); diff --git a/frontend/src/issues-list-board/issues-list-card.tsx b/frontend/src/issues-list-board/issues-list-card.tsx index ea49df0..6e52c1b 100644 --- a/frontend/src/issues-list-board/issues-list-card.tsx +++ b/frontend/src/issues-list-board/issues-list-card.tsx @@ -64,7 +64,7 @@ export const IssuesListCard = observer((props: Props): JSX.Element => { {props.store.status.name} - {props.store.priority.name} + {props.store?.priority?.name}
    diff --git a/frontend/src/issues-list-board/store.ts b/frontend/src/issues-list-board/store.ts index 6fbfe73..0d5de8f 100644 --- a/frontend/src/issues-list-board/store.ts +++ b/frontend/src/issues-list-board/store.ts @@ -30,9 +30,9 @@ export type IBoardStore = Instance; export const PageStore = types .model({ - loaded: types.boolean, - type: types.string, - name: types.string, + loaded: false, + type: '', + name: '', data: types.maybeNull(types.array(BoardStore)), }) .actions((self) => { diff --git a/frontend/src/kanban-board/store.ts b/frontend/src/kanban-board/store.ts index cfaeaf4..2fb1600 100644 --- a/frontend/src/kanban-board/store.ts +++ b/frontend/src/kanban-board/store.ts @@ -60,6 +60,7 @@ export const PageStore = types .actions((self) => { return { setData: (data: any) => { + console.debug('Kanban page store new data -', data); // DEBUG self.data = data; self.loaded = true; }, diff --git a/frontend/src/utils/style.ts b/frontend/src/utils/style.ts index 268f4fc..1908318 100644 --- a/frontend/src/utils/style.ts +++ b/frontend/src/utils/style.ts @@ -13,6 +13,7 @@ const formatStringToCamelCase = (str: string): string => { export const getStyleObjectFromString = ( str: string, ): Record => { + if (!str) return {}; const style = {} as Record; str.split(';').forEach((el) => { const [property, value] = el.split(':'); diff --git a/libs/event-emitter/src/dashboards/dashboards-data.service.ts b/libs/event-emitter/src/dashboards/dashboards-data.service.ts index 484be8b..9f68d94 100644 --- a/libs/event-emitter/src/dashboards/dashboards-data.service.ts +++ b/libs/event-emitter/src/dashboards/dashboards-data.service.ts @@ -56,7 +56,7 @@ export class DashboardsDataService { widget.dataLoaderParams, cfg, ); - if (loadRes.result) return { widget: widget, data: loadRes.error }; + if (loadRes.result) return { widget: widget, data: loadRes.result }; throw createAppError('CANNOT_LOAD_DATA'); } From f02d13e3dc80eac74f24529e5d92c1959998970b Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Sat, 4 Nov 2023 18:17:35 +0700 Subject: [PATCH 12/15] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B8=D0=B1=D1=8B=D1=82=D0=BE=D1=87=D0=BD=D0=B0=D1=8F=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B2=D0=B8=D0=B4=D0=B6=D0=B5=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/dashboard/dashboard-store.tsx | 8 ++++---- frontend/src/dashboard/widgets/issues-list.tsx | 2 +- frontend/src/dashboard/widgets/kanban.tsx | 2 +- .../src/issues-list-board/issues-list-board.tsx | 15 ++++++++++++--- .../src/dashboards/dashboards-data.service.ts | 11 ++++------- .../src/dashboards/interactive-widget-factory.ts | 8 +------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/frontend/src/dashboard/dashboard-store.tsx b/frontend/src/dashboard/dashboard-store.tsx index 594b55b..6342a05 100644 --- a/frontend/src/dashboard/dashboard-store.tsx +++ b/frontend/src/dashboard/dashboard-store.tsx @@ -1,5 +1,5 @@ import axios from 'axios'; -import { IAnyModelType, Instance, types } from 'mobx-state-tree'; +import { Instance, types } from 'mobx-state-tree'; type _WidgetParams = Record | null; @@ -90,7 +90,7 @@ export const Dashboard = types setWidgetsData: (data: any) => { for (let i = 0; i < data.length; i++) { const widgetData = data[i]?.data; - const widgetId = data[i]?.widget?.id; + const widgetId = data[i]?.widgetId; if (!widgetId || !widgetData) continue; const widgets = self.data?.widgets; @@ -147,8 +147,8 @@ export async function WidgetLoadData(widget: IWidget): Promise { const resp = await fetch(url); if (resp && resp.ok) { const data = await resp.json(); - if (data?.data) { - widget.setData(data?.data); + if (data) { + widget.setData(data); } } } diff --git a/frontend/src/dashboard/widgets/issues-list.tsx b/frontend/src/dashboard/widgets/issues-list.tsx index 3cbdcd4..d825576 100644 --- a/frontend/src/dashboard/widgets/issues-list.tsx +++ b/frontend/src/dashboard/widgets/issues-list.tsx @@ -12,7 +12,7 @@ export type Props = { export const IssuesList = observer((props: Props): JSX.Element => { const store = IssuesListBoardsStoreNs.PageStore.create({ loaded: false }); onSnapshot(props.store, (state) => { - if (state?.data?.data) store.setData(state?.data?.data); + if (state?.data) store.setData(state?.data); }); return ; }); diff --git a/frontend/src/dashboard/widgets/kanban.tsx b/frontend/src/dashboard/widgets/kanban.tsx index d86f8ec..473fbfc 100644 --- a/frontend/src/dashboard/widgets/kanban.tsx +++ b/frontend/src/dashboard/widgets/kanban.tsx @@ -11,7 +11,7 @@ export type Props = { export const Kanban = observer((props: Props): JSX.Element => { const store = KanbanBoardsStoreNs.PageStore.create({ loaded: false }); onSnapshot(props.store, (state) => { - if (state?.data?.data) store.setData(state?.data?.data); + if (state?.data) store.setData(state?.data); }); return ; }); diff --git a/frontend/src/issues-list-board/issues-list-board.tsx b/frontend/src/issues-list-board/issues-list-board.tsx index 01ba1c5..f589bc5 100644 --- a/frontend/src/issues-list-board/issues-list-board.tsx +++ b/frontend/src/issues-list-board/issues-list-board.tsx @@ -9,9 +9,18 @@ export type Props = { }; export const IssuesListBoard = observer((props: Props): JSX.Element => { - const list: JSX.Element[] = props.store.data.map((issue) => { - return ; - }); + const list: JSX.Element[] = []; + const data: any = props.store.data; + for (let i = 0; i < data.length; i++) { + const column = data[i]; + const issues: any[] = column.issues; + for (let j = 0; j < issues.length; j++) { + const issue = issues[j]; + list.push( + , + ); + } + } let title: JSX.Element; if (props.store.metainfo.url) { title = {props.store.metainfo.title}; diff --git a/libs/event-emitter/src/dashboards/dashboards-data.service.ts b/libs/event-emitter/src/dashboards/dashboards-data.service.ts index 9f68d94..9518305 100644 --- a/libs/event-emitter/src/dashboards/dashboards-data.service.ts +++ b/libs/event-emitter/src/dashboards/dashboards-data.service.ts @@ -5,7 +5,7 @@ import { AppError, Result, createAppError, fail } from '../utils/result'; import { WidgetsCollectionService } from './widgets-collection.service'; export type WidgetWithData = { - widget: DashboardModel.Widget; + widgetId: string; data: any; }; @@ -34,17 +34,14 @@ export class DashboardsDataService { if (loadRes.result) { isSuccess = true; loadRes.result.widgetId = widget.id; - results.push({ data: loadRes.result, widget: widget }); + results.push({ data: loadRes.result, widgetId: widget.id }); } } if (!isSuccess) throw createAppError('CANNOT_LOAD_DATA'); return results; } - async loadDataForWidget( - id: string, - widgetId: string, - ): Promise { + async loadDataForWidget(id: string, widgetId: string): Promise { const cfg = await this.dashboardsService.load(id); const widget = cfg.widgets.find((widget) => { return widget.id == widgetId; @@ -56,7 +53,7 @@ export class DashboardsDataService { widget.dataLoaderParams, cfg, ); - if (loadRes.result) return { widget: widget, data: loadRes.result }; + if (loadRes.result) return loadRes.result; throw createAppError('CANNOT_LOAD_DATA'); } diff --git a/libs/event-emitter/src/dashboards/interactive-widget-factory.ts b/libs/event-emitter/src/dashboards/interactive-widget-factory.ts index 7e8876e..1125f6e 100644 --- a/libs/event-emitter/src/dashboards/interactive-widget-factory.ts +++ b/libs/event-emitter/src/dashboards/interactive-widget-factory.ts @@ -16,13 +16,7 @@ export class InteractiveWidget dashboardParams: any, ): Promise> { const data = await this.dataLoader.load(dataLoaderParams, dashboardParams); - return data.error - ? fail(data.error) - : success({ - data: data.result, - widgetParams: widgetParams, - dashboardParams: dashboardParams, - }); + return data.error ? fail(data.error) : success(data.result); } } From 6925fde0e9caf9a5572155f9e8426918a41c71bd Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Sat, 4 Nov 2023 18:51:27 +0700 Subject: [PATCH 13/15] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20-=20=D1=82=D0=B8=D0=BF=20?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=80=D0=B0=20=D0=B4=D0=BB=D1=8F=20issues-li?= =?UTF-8?q?st=20=D0=B1=D0=B5=D1=80=D1=91=D1=82=D1=81=D1=8F=20=D0=BE=D1=82?= =?UTF-8?q?=20kanban=20=D0=B4=D0=BE=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/dashboard/widgets/issues-list.tsx | 4 ++-- frontend/src/issues-list-board/issues-list-board.tsx | 6 +++--- frontend/src/issues-list-board/issues-list-boards-page.tsx | 6 +++--- frontend/src/issues-list-board/issues-list-boards.tsx | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/dashboard/widgets/issues-list.tsx b/frontend/src/dashboard/widgets/issues-list.tsx index d825576..c607cb3 100644 --- a/frontend/src/dashboard/widgets/issues-list.tsx +++ b/frontend/src/dashboard/widgets/issues-list.tsx @@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; import * as DashboardStoreNs from '../dashboard-store'; import * as IssuesListBoardsNs from '../../issues-list-board/issues-list-boards'; -import * as IssuesListBoardsStoreNs from '../../issues-list-board/store'; +import * as KanbanBoardStoreNs from '../../kanban-board/store'; import { onSnapshot } from 'mobx-state-tree'; export type Props = { @@ -10,7 +10,7 @@ export type Props = { }; export const IssuesList = observer((props: Props): JSX.Element => { - const store = IssuesListBoardsStoreNs.PageStore.create({ loaded: false }); + const store = KanbanBoardStoreNs.PageStore.create({ loaded: false }); onSnapshot(props.store, (state) => { if (state?.data) store.setData(state?.data); }); diff --git a/frontend/src/issues-list-board/issues-list-board.tsx b/frontend/src/issues-list-board/issues-list-board.tsx index f589bc5..66da274 100644 --- a/frontend/src/issues-list-board/issues-list-board.tsx +++ b/frontend/src/issues-list-board/issues-list-board.tsx @@ -1,16 +1,16 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; -import { IBoardStore } from './store'; +import * as KanbanBoardStoreNs from '../kanban-board/store'; import Css from './issues-list-board.module.css'; import * as IssuesListCardNs from './issues-list-card'; export type Props = { - store: IBoardStore; + store: KanbanBoardStoreNs.IBoardStore; }; export const IssuesListBoard = observer((props: Props): JSX.Element => { const list: JSX.Element[] = []; - const data: any = props.store.data; + const data = props.store.data; for (let i = 0; i < data.length; i++) { const column = data[i]; const issues: any[] = column.issues; diff --git a/frontend/src/issues-list-board/issues-list-boards-page.tsx b/frontend/src/issues-list-board/issues-list-boards-page.tsx index 0474c63..702d5fc 100644 --- a/frontend/src/issues-list-board/issues-list-boards-page.tsx +++ b/frontend/src/issues-list-board/issues-list-boards-page.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import * as IssuesListStoreNs from './store'; +import * as KanbanBoardStoreNs from '../kanban-board/store'; import * as IssuesListBoardsNs from './issues-list-boards'; export const IssuesListBoardPage = (): JSX.Element => { @@ -15,12 +15,12 @@ export const IssuesListBoardPage = (): JSX.Element => { }); // DEBUG: end - const store = IssuesListStoreNs.PageStore.create({ + const store = KanbanBoardStoreNs.PageStore.create({ loaded: false, type: type, name: name, }); - IssuesListStoreNs.PageStoreLoadData(store); + KanbanBoardStoreNs.PageStoreLoadData(store); return ; }; diff --git a/frontend/src/issues-list-board/issues-list-boards.tsx b/frontend/src/issues-list-board/issues-list-boards.tsx index b77c2db..18ac96a 100644 --- a/frontend/src/issues-list-board/issues-list-boards.tsx +++ b/frontend/src/issues-list-board/issues-list-boards.tsx @@ -1,13 +1,13 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; -import * as IssuesListBoardStore from './store'; +import * as KanbanBoardStoreNs from '../kanban-board/store'; import * as IssuesListBoardNs from './issues-list-board'; import * as TopRightMenuNs from '../misc-components/top-right-menu'; import { SetIssuesReadingTimestamp } from '../utils/unreaded-provider'; import * as ServiceActionsButtons from '../utils/service-actions-buttons'; export type Props = { - store: IssuesListBoardStore.IPageStore; + store: KanbanBoardStoreNs.IPageStore; }; export const IssuesListBoards = observer((props: Props): JSX.Element => { @@ -28,7 +28,7 @@ export const IssuesListBoards = observer((props: Props): JSX.Element => { const onAllReadItemClick = (e: React.MouseEvent) => { e.stopPropagation(); SetIssuesReadingTimestamp(props.store.issueIds); - IssuesListBoardStore.PageStoreLoadData(props.store); + KanbanBoardStoreNs.PageStoreLoadData(props.store); }; return ( <> From 8f36e6cca2091ca756d1812b5f9771a293408f4c Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Tue, 7 Nov 2023 01:09:00 +0700 Subject: [PATCH 14/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=80=D0=B5=D0=B4=D0=BA=D0=B0=D1=82=D0=BE=D1=80?= =?UTF-8?q?=20=D0=B4=D0=B0=D1=88=D0=B1=D0=BE=D1=80=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 63 ++++++++++++ frontend/package.json | 1 + frontend/src/dashboard/dashboard.tsx | 10 ++ frontend/src/dashboard/editor.module.css | 27 +++++ frontend/src/dashboard/editor.tsx | 122 +++++++++++++++++++++++ 5 files changed, 223 insertions(+) create mode 100644 frontend/src/dashboard/editor.module.css create mode 100644 frontend/src/dashboard/editor.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f2feaa5..f00f493 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@monaco-editor/react": "^4.6.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -2977,6 +2978,30 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -11945,6 +11970,12 @@ "mobx": "^6.3.0" } }, + "node_modules/monaco-editor": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", + "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==", + "peer": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -15205,6 +15236,11 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -19072,6 +19108,22 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, + "@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "requires": { + "state-local": "^1.0.6" + } + }, + "@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "requires": { + "@monaco-editor/loader": "^1.4.0" + } + }, "@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -25596,6 +25648,12 @@ "integrity": "sha512-oe82BNgMr408e6DxMDNat8msXQTuyuqzJ97DPupbhchEfjjHyjsmPSwtXHl+nXiW3tybpb/cr5siUClBqKqv+Q==", "requires": {} }, + "monaco-editor": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", + "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==", + "peer": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -27772,6 +27830,11 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, + "state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index d5577bd..e0008f5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@monaco-editor/react": "^4.6.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/frontend/src/dashboard/dashboard.tsx b/frontend/src/dashboard/dashboard.tsx index 654e996..31bfa3d 100644 --- a/frontend/src/dashboard/dashboard.tsx +++ b/frontend/src/dashboard/dashboard.tsx @@ -3,6 +3,7 @@ import React from 'react'; import * as DashboardStoreNs from './dashboard-store'; import * as TopRightMenuNs from '../misc-components/top-right-menu'; import * as WidgetNs from './widget'; +import * as EditorNs from './editor'; export type Props = { store: DashboardStoreNs.IDashboard }; @@ -19,11 +20,20 @@ export const Dashboard = observer((props: Props): JSX.Element => { return ; }); + const editorStore = EditorNs.Store.create({ dashboardId: props.store.id }); + const onEditClick = (e: React.MouseEvent) => { + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + editorStore.show(); + }; + const res = (
    + Назад Дашборд - {props.store.data?.title || props.store.id} + {widgets}
    diff --git a/frontend/src/dashboard/editor.module.css b/frontend/src/dashboard/editor.module.css new file mode 100644 index 0000000..d01f80c --- /dev/null +++ b/frontend/src/dashboard/editor.module.css @@ -0,0 +1,27 @@ +.reset { + all: initial; +} + +.modal { + z-index: 1000; + position: fixed; + display: none; + padding-top: 40px; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgb(0, 0, 0); + background-color: rgba(0, 0, 0, 0.4); +} + +.modalContent { + height: 80%; + overflow: auto; + background-color: #fefefe; + margin: auto; + padding: 20px; + border: 1px solid #888; + width: 80%; +} \ No newline at end of file diff --git a/frontend/src/dashboard/editor.tsx b/frontend/src/dashboard/editor.tsx new file mode 100644 index 0000000..c2953d5 --- /dev/null +++ b/frontend/src/dashboard/editor.tsx @@ -0,0 +1,122 @@ +import React, { useState } from 'react'; +import { Editor as MonacoEditor } from '@monaco-editor/react'; +import { observer } from 'mobx-react-lite'; +import { Instance, onSnapshot, types } from 'mobx-state-tree'; +import Css from './editor.module.css'; + +export const Store = types + .model({ + loaded: false, + dashboardId: '', + visible: false, + data: '', + }) + .actions((self) => { + return { + setData: (data: string) => { + self.loaded = true; + self.data = data; + }, + show: () => { + if (!self.loaded) LoadDashboardToStore(self as any); + self.visible = true; + }, + hide: () => { + self.visible = false; + }, + toggleVisible: () => { + self.visible = !self.visible; + }, + }; + }) + .views((self) => { + return { + get displayStyle(): React.CSSProperties { + return { display: self.visible ? 'block' : 'none' }; + }, + }; + }); + +type IStore = Instance; + +export async function LoadDashboard(dashboardId: string): Promise { + const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${dashboardId}`; + const resp = await fetch(url); + if (!resp || !resp.ok) return ''; + const data = await resp.json(); + const text = JSON.stringify(data, null, ' '); + return text; +} + +export async function LoadDashboardToStore(store: IStore): Promise { + const data = await LoadDashboard(store.dashboardId); + if (data) store.setData(data); +} + +export async function SaveDashboard( + dashboardId: string, + data: string, +): Promise { + const url = `${process.env.REACT_APP_BACKEND}api/dashboard/${dashboardId}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: data, + }); +} + +export async function SaveDashboardFromStore(store: IStore): Promise { + await SaveDashboard(store.dashboardId, store.data); +} + +export type Props = { + store: IStore; +}; + +export const Editor = observer((props: Props): JSX.Element => { + const onCloseClick = (e: React.MouseEvent) => { + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + props.store.hide(); + }; + const onSaveClick = async (e: React.MouseEvent) => { + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + props.store.setData(editorValue); + await SaveDashboardFromStore(props.store); + alert('Сохранено'); + }; + + const [editorValue, setEditorValue] = useState(props.store.data); + onSnapshot(props.store, (state) => { + setEditorValue(state.data); + }); + + return ( +
    +
    +
    +

    + + + Редактор дашборда +

    + setEditorValue(value || '')} + > +
    +
    + Editor +
    + ); +}); From c54baec4ed6688a1cda1f27ff0b99cf5922d1cab Mon Sep 17 00:00:00 2001 From: Pavel Gnedov Date: Tue, 7 Nov 2023 01:50:59 +0700 Subject: [PATCH 15/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=B4=D0=B0=D1=88=D0=B1=D0=BE=D1=80=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/dashboard/dashboards-page.tsx | 43 ++++++++++++++++++- .../src/dashboards/dashboards-data.service.ts | 3 ++ .../src/dashboards/dashboards.service.ts | 2 +- libs/event-emitter/src/models/dashboard.ts | 2 +- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/frontend/src/dashboard/dashboards-page.tsx b/frontend/src/dashboard/dashboards-page.tsx index d251890..7a7620e 100644 --- a/frontend/src/dashboard/dashboards-page.tsx +++ b/frontend/src/dashboard/dashboards-page.tsx @@ -6,5 +6,46 @@ export const DashboardsPage = (): JSX.Element => { const store = Store.List.create({ loaded: false, list: [] }); Store.ListStoreLoadData(store); - return ; + const onNewDashboardButtonClick = async (e: React.MouseEvent) => { + if (e.target !== e.currentTarget) return; + e.stopPropagation(); + const createUrl = `${process.env.REACT_APP_BACKEND}api/dashboard`; + const createResp = await fetch(createUrl, { + method: 'POST', + }); + if (!createResp || !createResp.ok) { + alert(`Ошибка - Не удалось создать новый дашборд`); + return; + } + const dashboardId = await createResp.text(); + const dashboardName = prompt( + `Dashboard name for dashboardId = ${dashboardId}`, + ); + if (dashboardName) { + const modifyUrl = `${process.env.REACT_APP_BACKEND}api/dashboard/${dashboardId}`; + const modifyResp = await fetch(modifyUrl, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ widgets: [], title: dashboardName }), + }); + if (!modifyResp || !modifyResp.ok) { + alert(`Не удалось выполнить создание дашборда`); + } else { + alert(`Создан дашборд ${dashboardName}`); + } + return; + } else { + alert(`Создан анонимный дашборд ${dashboardId}`); + return; + } + }; + + return ( + <> + + + + ); }; diff --git a/libs/event-emitter/src/dashboards/dashboards-data.service.ts b/libs/event-emitter/src/dashboards/dashboards-data.service.ts index 9518305..f240bd1 100644 --- a/libs/event-emitter/src/dashboards/dashboards-data.service.ts +++ b/libs/event-emitter/src/dashboards/dashboards-data.service.ts @@ -22,6 +22,9 @@ export class DashboardsDataService { const cfg = await this.dashboardsService.load(id); const results: WidgetWithData[] = []; let isSuccess = false; + if (!cfg?.widgets || cfg?.widgets?.length <= 0) { + return results; + } for (let i = 0; i < cfg.widgets.length; i++) { const widget = cfg.widgets[i]; if (widget.collapsed) continue; diff --git a/libs/event-emitter/src/dashboards/dashboards.service.ts b/libs/event-emitter/src/dashboards/dashboards.service.ts index dade61b..425cde7 100644 --- a/libs/event-emitter/src/dashboards/dashboards.service.ts +++ b/libs/event-emitter/src/dashboards/dashboards.service.ts @@ -42,7 +42,7 @@ export class DashboardsService { async load(id: string): Promise { this.logger.debug(`Load dashboard id - ${id}`); const rawData = await this.loadRawData(id); - return rawData.data; + return rawData?.data || { widgets: [] }; } async isExists(id: string): Promise { diff --git a/libs/event-emitter/src/models/dashboard.ts b/libs/event-emitter/src/models/dashboard.ts index 88e1889..fc07929 100644 --- a/libs/event-emitter/src/models/dashboard.ts +++ b/libs/event-emitter/src/models/dashboard.ts @@ -1,6 +1,6 @@ export type Data = { widgets: Widget[]; - title: string; + title?: string; } | null; export type Dashboard = {