Compare commits
3 commits
04a0736c9a
...
8479b6d68d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8479b6d68d | ||
|
|
77352a497b | ||
|
|
20b70c5799 |
8 changed files with 243 additions and 15 deletions
|
|
@ -24,7 +24,7 @@
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "BROWSER=none react-scripts start --open=false",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
|
|
|
||||||
146
frontend/src/dashboard/widgets/daily-eccm-v2.tsx
Normal file
146
frontend/src/dashboard/widgets/daily-eccm-v2.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import React from 'react';
|
||||||
|
import * as DashboardStoreNs from '../dashboard-store';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { DebugInfo } from '../../misc-components/debug-info';
|
||||||
|
import { Instance, onSnapshot, types } from 'mobx-state-tree';
|
||||||
|
|
||||||
|
export const DailyEccmV2Data = types.model({
|
||||||
|
id: types.string,
|
||||||
|
dashboardId: types.string,
|
||||||
|
widgetId: types.string,
|
||||||
|
datetime: types.number,
|
||||||
|
datetimeFormatted: types.string,
|
||||||
|
reportIssues: types.array(types.frozen()),
|
||||||
|
issuesMetrics: types.frozen(),
|
||||||
|
latest: types.boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DailyEccmV2Store = types
|
||||||
|
.model({
|
||||||
|
data: DailyEccmV2Data,
|
||||||
|
})
|
||||||
|
.actions((self) => ({
|
||||||
|
setData(data: any) {
|
||||||
|
self.data = data;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type IDailyEccmV2Store = Instance<typeof DailyEccmV2Store>;
|
||||||
|
|
||||||
|
export const DailyEccmV2 = observer(
|
||||||
|
(props: { store: IDailyEccmV2Store }): JSX.Element => {
|
||||||
|
const dashboardId = props.store?.data?.dashboardId;
|
||||||
|
const widgetId = props.store?.data?.widgetId;
|
||||||
|
const keyPrefix = `dashboard-${dashboardId}-widget-${widgetId}`;
|
||||||
|
|
||||||
|
const issuesByStatusCount: Record<string, any> =
|
||||||
|
props.store?.data?.issuesMetrics?.issuesByStatusCount ?? {};
|
||||||
|
const issuesByVersionsCount: Record<string, any> =
|
||||||
|
props.store?.data?.issuesMetrics?.issuesByVersionsCount ?? {};
|
||||||
|
const issuesByUsername: Record<string, any> =
|
||||||
|
props.store?.data?.issuesMetrics?.issuesByUsername ?? {};
|
||||||
|
|
||||||
|
const byStatusLi: JSX.Element[] = [];
|
||||||
|
|
||||||
|
Object.keys(issuesByStatusCount).forEach((status) => {
|
||||||
|
let byStatusHint: JSX.Element;
|
||||||
|
const byVersion = issuesByStatusCount[status]?.byVersion;
|
||||||
|
const keyPrefixForCurrentStatusBlock = `${keyPrefix}-issuesByStatusCount-${status}`;
|
||||||
|
if (byVersion && Object.keys(byVersion).length > 0) {
|
||||||
|
const byVersionSimple = Object.keys(byVersion).reduce(
|
||||||
|
(acc: Record<string, number>, version) => {
|
||||||
|
acc[version] = byVersion[version].count;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, number>,
|
||||||
|
);
|
||||||
|
byStatusHint = (
|
||||||
|
<span
|
||||||
|
key={`${keyPrefixForCurrentStatusBlock}-byVersionHint`}
|
||||||
|
style={{ marginLeft: '10px', color: 'lightgray' }}
|
||||||
|
>
|
||||||
|
(Версии: {JSON.stringify(byVersionSimple)})
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
byStatusHint = <></>;
|
||||||
|
}
|
||||||
|
byStatusLi.push(
|
||||||
|
<li key={`${keyPrefixForCurrentStatusBlock}`}>
|
||||||
|
{status}: {issuesByStatusCount[status].count} {byStatusHint}
|
||||||
|
</li>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const byVersionsLi: JSX.Element[] = [];
|
||||||
|
|
||||||
|
Object.keys(issuesByVersionsCount).forEach((version) => {
|
||||||
|
const byStatus = issuesByVersionsCount[version].byStatus;
|
||||||
|
const keyPrefixForCurrentVersionBlock = `${keyPrefix}-issuesByVersionsCount-${version}`;
|
||||||
|
|
||||||
|
let byStatusHint: JSX.Element = <></>;
|
||||||
|
if (byStatus && Object.keys(byStatus).length > 0) {
|
||||||
|
const byStatusSimple = Object.keys(byStatus).reduce((acc, status) => {
|
||||||
|
acc[status] = byStatus[status].count;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
byStatusHint = (
|
||||||
|
<span
|
||||||
|
style={{ marginLeft: '10px', color: 'lightgray' }}
|
||||||
|
key={`${keyPrefixForCurrentVersionBlock}-byStatusHint`}
|
||||||
|
>
|
||||||
|
(Статусы: {JSON.stringify(byStatusSimple)})
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
byVersionsLi.push(
|
||||||
|
<li key={keyPrefixForCurrentVersionBlock}>
|
||||||
|
{version}: {issuesByVersionsCount[version].count} {byStatusHint}
|
||||||
|
</li>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Daily ECCM V2</h1>
|
||||||
|
<p>Дата выгрузки: {props.store?.data?.datetimeFormatted}</p>
|
||||||
|
<p>Метрики:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Простые счётчики:
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
По статусам:
|
||||||
|
<ul>{byStatusLi}</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
По версиям:
|
||||||
|
<ul>{byVersionsLi}</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<DebugInfo value={JSON.stringify(props.store?.data, null, 2)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
store: DashboardStoreNs.IWidget;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DailyEccmV2Widget = observer((props: Props): JSX.Element => {
|
||||||
|
const dailyEccmV2Store = DailyEccmV2Store.create();
|
||||||
|
onSnapshot(props.store, (storeState) => {
|
||||||
|
if (storeState.data) {
|
||||||
|
dailyEccmV2Store.setData(storeState.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DailyEccmV2 store={dailyEccmV2Store} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -6,6 +6,7 @@ import * as KanbanWidgetNs from './kanban';
|
||||||
import { DebugInfo } from '../../misc-components/debug-info';
|
import { DebugInfo } from '../../misc-components/debug-info';
|
||||||
import * as IssuesListNs from './issues-list';
|
import * as IssuesListNs from './issues-list';
|
||||||
import * as CalendarNextEventsNs from './calendar-next-events';
|
import * as CalendarNextEventsNs from './calendar-next-events';
|
||||||
|
import * as DailyEccmV2 from './daily-eccm-v2';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
store: Instance<typeof DashboardStoreNs.Widget>;
|
store: Instance<typeof DashboardStoreNs.Widget>;
|
||||||
|
|
@ -26,6 +27,10 @@ export const WidgetFactory = observer((props: Props): JSX.Element => {
|
||||||
return <CalendarNextEventsNs.CalendarNextEvents store={props.store} />;
|
return <CalendarNextEventsNs.CalendarNextEvents store={props.store} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'daily_eccm_v2') {
|
||||||
|
return <DailyEccmV2.DailyEccmV2 store={props.store} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>Unknown widget</div>
|
<div>Unknown widget</div>
|
||||||
|
|
|
||||||
|
|
@ -119,11 +119,19 @@ export class RedmineDataLoader {
|
||||||
skip_empty_lines: true,
|
skip_empty_lines: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const resp = await fetch(urlQuery, {
|
let resp;
|
||||||
headers: {
|
try {
|
||||||
'X-Redmine-API-Key': this.redmineToken,
|
resp = await fetch(urlQuery, {
|
||||||
},
|
headers: {
|
||||||
});
|
'X-Redmine-API-Key': this.redmineToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to fetch data from Redmine by url ${urlQuery}: ${err}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const rawData = await resp.text();
|
const rawData = await resp.text();
|
||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import { TelegramBotController } from './telegram-bot/telegram-bot.controller';
|
||||||
import { DailyEccmV2ReportTaskRunnerService } from './reports/daily-eccm-v2-report-task-runner.service';
|
import { DailyEccmV2ReportTaskRunnerService } from './reports/daily-eccm-v2-report-task-runner.service';
|
||||||
import { DashboardsService } from '@app/event-emitter/dashboards/dashboards.service';
|
import { DashboardsService } from '@app/event-emitter/dashboards/dashboards.service';
|
||||||
import { DailyEccmReportsV2Datasource } from './couchdb-datasources/daily-eccm-reports-v2.datasource';
|
import { DailyEccmReportsV2Datasource } from './couchdb-datasources/daily-eccm-reports-v2.datasource';
|
||||||
|
import { DailyEccmReportsV2DataLoaderService } from './eccm-statistic/dashboards/widget-data-loader/daily-eccm-v2.widget-data-loader.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -121,6 +122,7 @@ import { DailyEccmReportsV2Datasource } from './couchdb-datasources/daily-eccm-r
|
||||||
DashboardsService,
|
DashboardsService,
|
||||||
DailyEccmReportsV2Datasource,
|
DailyEccmReportsV2Datasource,
|
||||||
DailyEccmV2ReportTaskRunnerService,
|
DailyEccmV2ReportTaskRunnerService,
|
||||||
|
DailyEccmReportsV2DataLoaderService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements OnModuleInit {
|
export class AppModule implements OnModuleInit {
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { WidgetsCollectionService } from '@app/event-emitter/dashboards/widgets-collection.service';
|
import { WidgetsCollectionService } from '@app/event-emitter/dashboards/widgets-collection.service';
|
||||||
import { IssuesByTagsWidgetDataLoaderService } from './widget-data-loader/issues-by-tags.widget-data-loader.service';
|
import { IssuesByTagsWidgetDataLoaderService } from './widget-data-loader/issues-by-tags.widget-data-loader.service';
|
||||||
import { createInteractiveWidget } from '@app/event-emitter/dashboards/interactive-widget-factory';
|
import { createInteractiveWidget } from '@app/event-emitter/dashboards/interactive-widget-factory';
|
||||||
|
import { DailyEccmReportsV2DataLoaderService } from 'src/eccm-statistic/dashboards/widget-data-loader/daily-eccm-v2.widget-data-loader.service';
|
||||||
|
import { WIDGET_TYPE as DAILY_ECCM_V2_WIDGET_TYPE } from 'src/reports/daily-eccm-v2-report-task-runner.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DashboardInitService {
|
export class DashboardInitService {
|
||||||
constructor(
|
constructor(
|
||||||
private issuesByTagsWidgetDataLoaderService: IssuesByTagsWidgetDataLoaderService,
|
private issuesByTagsWidgetDataLoaderService: IssuesByTagsWidgetDataLoaderService,
|
||||||
|
private dailyEccmReportsV2DataLoaderService: DailyEccmReportsV2DataLoaderService,
|
||||||
private widgetsCollectionService: WidgetsCollectionService,
|
private widgetsCollectionService: WidgetsCollectionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
@ -20,6 +23,10 @@ export class DashboardInitService {
|
||||||
this.issuesByTagsWidgetDataLoaderService,
|
this.issuesByTagsWidgetDataLoaderService,
|
||||||
'issues_list_by_tags',
|
'issues_list_by_tags',
|
||||||
),
|
),
|
||||||
|
createInteractiveWidget(
|
||||||
|
this.dailyEccmReportsV2DataLoaderService,
|
||||||
|
DAILY_ECCM_V2_WIDGET_TYPE,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
collection.forEach((w) => this.widgetsCollectionService.appendWidget(w));
|
collection.forEach((w) => this.widgetsCollectionService.appendWidget(w));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { WidgetDataLoaderInterface } from '@app/event-emitter/dashboards/widget-data-loader-interface';
|
||||||
|
import {
|
||||||
|
AppError,
|
||||||
|
createAppError,
|
||||||
|
fail,
|
||||||
|
Result,
|
||||||
|
success,
|
||||||
|
} from '@app/event-emitter/utils/result';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DailyEccmReportsV2Datasource } from 'src/couchdb-datasources/daily-eccm-reports-v2.datasource';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DailyEccmReportsV2DataLoaderService
|
||||||
|
implements WidgetDataLoaderInterface<any, any, any>
|
||||||
|
{
|
||||||
|
isMyConfig(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(
|
||||||
|
dataLoaderParams: any,
|
||||||
|
dashboardParams: any,
|
||||||
|
dashboardId: string,
|
||||||
|
widgetId: string,
|
||||||
|
): Promise<Result<any, AppError>> {
|
||||||
|
const ds = await DailyEccmReportsV2Datasource.getDatasource();
|
||||||
|
const response = await ds.find({
|
||||||
|
selector: {
|
||||||
|
dashboardId: dashboardId,
|
||||||
|
widgetId: widgetId,
|
||||||
|
latest: true,
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
const data = response.docs[0];
|
||||||
|
if (!data) {
|
||||||
|
return fail(createAppError('No data found'));
|
||||||
|
}
|
||||||
|
return success(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -229,8 +229,26 @@ export class DailyEccmV2ReportTaskRunnerService {
|
||||||
JSON.stringify(issuesByVersionsCount),
|
JSON.stringify(issuesByVersionsCount),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ШАГ 3. Подсчет количества внутренних изменений
|
// ШАГ 3. Подсчёт количества задач по работникам
|
||||||
this.logger.debug('Step 3. Calculating internal changes');
|
this.logger.debug('Step 3. Calculating issues by user names');
|
||||||
|
const issuesByUsername = {};
|
||||||
|
issues.forEach((issue: any) => {
|
||||||
|
const currentUser = issue.current_user?.name;
|
||||||
|
if (currentUser) {
|
||||||
|
if (!issuesByUsername[currentUser]) {
|
||||||
|
issuesByUsername[currentUser] = { count: 0, issueIds: [] };
|
||||||
|
}
|
||||||
|
issuesByUsername[currentUser].count += 1;
|
||||||
|
issuesByUsername[currentUser].issueIds.push(issue.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.logger.debug(
|
||||||
|
'Step 3. Calculating issues by user names - done',
|
||||||
|
JSON.stringify(issuesByUsername),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ШАГ 4. Подсчет количества внутренних изменений
|
||||||
|
this.logger.debug('Step 4. Calculating internal changes');
|
||||||
const changesInterval =
|
const changesInterval =
|
||||||
(widget?.dataLoaderParams['changesInterval'] as number) ??
|
(widget?.dataLoaderParams['changesInterval'] as number) ??
|
||||||
24 * 60 * 60 * 1000;
|
24 * 60 * 60 * 1000;
|
||||||
|
|
@ -300,12 +318,12 @@ export class DailyEccmV2ReportTaskRunnerService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Step 3. Calculating internal changes - done',
|
'Step 4. Calculating internal changes - done',
|
||||||
JSON.stringify(changesCount),
|
JSON.stringify(changesCount),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ШАГ 4: Количество задач с оценками трудозатрат
|
// ШАГ 5: Количество задач с оценками трудозатрат
|
||||||
this.logger.debug('Step 4. Counting issues with estimates and spent hours');
|
this.logger.debug('Step 5. Counting issues with estimates and spent hours');
|
||||||
const issuesWithEstimatesAndSpenthoursCount = {
|
const issuesWithEstimatesAndSpenthoursCount = {
|
||||||
withEstimates: { count: 0, issueIds: [] },
|
withEstimates: { count: 0, issueIds: [] },
|
||||||
withoutEstimates: { count: 0, issueIds: [] },
|
withoutEstimates: { count: 0, issueIds: [] },
|
||||||
|
|
@ -402,12 +420,12 @@ export class DailyEccmV2ReportTaskRunnerService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Step 4. Counting issues with estimates and spent hours - done',
|
'Step 5. Counting issues with estimates and spent hours - done',
|
||||||
JSON.stringify(issuesWithEstimatesAndSpenthoursCount),
|
JSON.stringify(issuesWithEstimatesAndSpenthoursCount),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ШАГ 5: Счётчики сравнения с предыдущим отчётом
|
// ШАГ 6: Счётчики сравнения с предыдущим отчётом
|
||||||
this.logger.debug('Step 5: Calculating differences with previous report');
|
this.logger.debug('Step 6: Calculating differences with previous report');
|
||||||
const differencesCount = {
|
const differencesCount = {
|
||||||
newIssues: { count: 0, issueIds: [] },
|
newIssues: { count: 0, issueIds: [] },
|
||||||
lostIssues: { count: 0, issueIds: [] },
|
lostIssues: { count: 0, issueIds: [] },
|
||||||
|
|
@ -461,13 +479,14 @@ export class DailyEccmV2ReportTaskRunnerService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Step 5: Calculating differences with previous report - done',
|
'Step 6: Calculating differences with previous report - done',
|
||||||
JSON.stringify(differencesCount),
|
JSON.stringify(differencesCount),
|
||||||
);
|
);
|
||||||
|
|
||||||
const metrics = {
|
const metrics = {
|
||||||
issuesByStatusCount: issuesByStatusCount,
|
issuesByStatusCount: issuesByStatusCount,
|
||||||
issuesByVersionsCount: issuesByVersionsCount,
|
issuesByVersionsCount: issuesByVersionsCount,
|
||||||
|
issuesByUsername: issuesByUsername,
|
||||||
changesCount: changesCount,
|
changesCount: changesCount,
|
||||||
issuesWithEstimatesAndSpenthoursCount:
|
issuesWithEstimatesAndSpenthoursCount:
|
||||||
issuesWithEstimatesAndSpenthoursCount,
|
issuesWithEstimatesAndSpenthoursCount,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue