Вывод всех метрик на виджет на дашборде

This commit is contained in:
Pavel Gnedov 2025-03-06 07:20:52 +07:00
parent d8f56b8c34
commit e4fb067ea9
3 changed files with 449 additions and 8 deletions

View file

@ -4,6 +4,7 @@ import { observer } from 'mobx-react-lite';
import { DebugInfo } from '../../misc-components/debug-info'; import { DebugInfo } from '../../misc-components/debug-info';
import { Instance, onSnapshot, types } from 'mobx-state-tree'; import { Instance, onSnapshot, types } from 'mobx-state-tree';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { text2id } from '../../utils/text-to-id';
export const DailyEccmV2Data = types.model({ export const DailyEccmV2Data = types.model({
id: types.string, id: types.string,
@ -29,7 +30,7 @@ export const DailyEccmV2Store = types
export type IDailyEccmV2Store = Instance<typeof DailyEccmV2Store>; export type IDailyEccmV2Store = Instance<typeof DailyEccmV2Store>;
const SimpleCounterViewComponent = (props: { const SimpleCounterViewComponent = (props: {
key?: string; id: string;
label: string; label: string;
count: number; count: number;
issueIds?: number[]; issueIds?: number[];
@ -38,41 +39,178 @@ const SimpleCounterViewComponent = (props: {
}): JSX.Element => { }): JSX.Element => {
let detailsHintComponent = <></>; let detailsHintComponent = <></>;
if (props.details) { if (props.details) {
const parentKey = props.key const parentKey = props.id
? props.key ? props.id
: `simple-counter-component-${uuidv4()}`; : `simple-counter-component-${uuidv4()}`;
const detailsKey = `${parentKey}-details`; const detailsKey = `${parentKey}-details`;
detailsHintComponent = ( detailsHintComponent = (
<DetailsHintViewComponent <DetailsHintViewComponent
key={detailsKey} key={detailsKey}
id={detailsKey}
mainLabel={props.detailsLabel ?? 'Details'} mainLabel={props.detailsLabel ?? 'Details'}
details={props.details} details={props.details}
/> />
); );
} }
return ( return (
<span key={props.key}> <span key={props.id}>
{props.label}: {props.count} {detailsHintComponent} {props.label}: {props.count} {detailsHintComponent}
</span> </span>
); );
}; };
const DetailsHintViewComponent = (props: { const DetailsHintViewComponent = (props: {
key?: string; id: string;
mainLabel: string; mainLabel: string;
details: Record<string, number>; details: Record<string, number>;
}): JSX.Element => { }): JSX.Element => {
return ( return (
<span key={props.key} style={{ marginLeft: '10px', color: 'lightgray' }}> <span key={props.id} style={{ marginLeft: '10px', color: 'lightgray' }}>
({props.mainLabel}: {JSON.stringify(props.details)}) ({props.mainLabel}: {JSON.stringify(props.details)})
</span> </span>
); );
}; };
const ChangesCounterTotalViewComponent = (props: {
id: string;
data: Record<string, any>;
}): JSX.Element => {
if (!props.id) return <></>;
const titles: Record<string, string> = {
byIssue: 'По задачам',
byStatus: 'По статусам',
byVersion: 'По версиям',
byUserName: 'По работникам',
};
const groups = Object.keys(titles);
const details: JSX.Element[] = [];
groups.forEach((groupName) => {
const groupKey = `${props.id}-details-${groupName}`;
const groupTitle = titles[groupName];
const groupData = props.data[groupName];
const cmp = (
<ChangesCounterListViewComponent
key={groupKey}
id={groupKey}
title={groupTitle}
data={groupData}
/>
);
details.push(cmp);
});
return (
<ul>
<li>Итого изменений: {props.data?.totalChanges ?? 0}</li>
<li>Итого комментариев: {props.data?.totalComments ?? 0}</li>
{details}
</ul>
);
};
const ChangesCounterListViewComponent = (props: {
id: string;
title: string;
data?: Record<string, { changes: number; comments: number }>;
}): JSX.Element => {
if (!props.data || !props.id) return <></>;
const dataKeys = Object.keys(props.data ?? {});
if (!dataKeys || dataKeys.length <= 0) return <></>;
const res: JSX.Element[] = [];
dataKeys.forEach((itemKey) => {
const itemData = props.data && props.data[itemKey];
if (!itemData) return;
const key = `${props.id}-${text2id(props.title)}-${text2id(itemKey)}`;
res.push(
<li key={`${key}-li`}>
<ChangesCounterItemViewComponent
key={key}
id={key}
label={itemKey}
type={itemKey === 'byIssue' ? 'issue' : 'other'}
changes={itemData.changes}
comments={itemData.comments}
/>
</li>,
);
});
return (
<>
{props.title}:<ul>{res}</ul>
</>
);
};
const ChangesCounterItemViewComponent = (props: {
id: string;
type: 'issue' | 'other';
label: string;
changes: number;
comments: number;
}): JSX.Element => {
return (
<span key={props.id}>
<span>
{props.type == 'issue' ? '#' : ''}
{props.label}:&nbsp;
</span>
<span>все изменения - {props.changes}</span>,&nbsp;
<span>комментарии - {props.comments}</span>
</span>
);
};
const VerySimpleCounterViewComponent = (props: {
id: string;
title: string;
value: number | string;
details?: any;
}): JSX.Element => {
return (
<span>
{props.title}:
<span style={{ marginLeft: '10px' }}>&nbsp;{props.value}</span>
<span style={{ marginLeft: '20px', color: 'lightgray' }}>
&nbsp;{props.details ? JSON.stringify(props.details) : ''}
</span>
</span>
);
};
const VerySimpleCounterListViewComponent = (props: {
id: string;
data: { title: string; value: number | string; details?: any }[];
}): JSX.Element => {
const items: JSX.Element[] = [];
props.data.forEach((item) => {
const itemId = `${props.id}-${text2id(item.title)}`;
items.push(
<VerySimpleCounterViewComponent
key={itemId}
id={itemId}
title={item.title}
value={item.value}
details={item.details}
/>,
);
});
return (
<ul key={props.id}>
{items.map((item) => (
<li key={item.props.id + '-li'}>{item}</li>
))}
</ul>
);
};
export const DailyEccmV2 = observer( export const DailyEccmV2 = observer(
(props: { store: IDailyEccmV2Store }): JSX.Element => { (props: { store: IDailyEccmV2Store }): JSX.Element => {
const dashboardId = props.store?.data?.dashboardId; const dashboardId = props.store?.data?.dashboardId;
const widgetId = props.store?.data?.widgetId; const widgetId = props.store?.data?.widgetId;
if (!dashboardId || !widgetId) return <></>;
const keyPrefix = `dashboard-${dashboardId}-widget-${widgetId}`; const keyPrefix = `dashboard-${dashboardId}-widget-${widgetId}`;
const issuesByStatusCount: Record<string, any> = const issuesByStatusCount: Record<string, any> =
@ -81,6 +219,233 @@ export const DailyEccmV2 = observer(
props.store?.data?.issuesMetrics?.issuesByVersionsCount ?? {}; props.store?.data?.issuesMetrics?.issuesByVersionsCount ?? {};
const issuesByUsername: Record<string, any> = const issuesByUsername: Record<string, any> =
props.store?.data?.issuesMetrics?.issuesByUsername ?? {}; props.store?.data?.issuesMetrics?.issuesByUsername ?? {};
const changesCount: Record<string, any> =
props.store?.data?.issuesMetrics?.changesCount ?? {};
const otherData: {
title: string;
value: number | string;
details?: any;
}[] = [];
// ШАГ 1. Количество задач по статусам
const issuesByStatusCount2 =
props.store?.data?.issuesMetrics?.issuesByStatusCount ?? {};
const issuesByStatusCount2Prefix = `Счётчики с количеством задач по статусам`;
Object.keys(issuesByStatusCount2).forEach((statusName) => {
const statusData = issuesByStatusCount2[statusName];
if (statusData) {
otherData.push({
title: `${issuesByStatusCount2Prefix} / ${statusName} / Количество задач`,
value: statusData.count,
details: {
issueIds: statusData.issueIds,
},
});
const byVersions = statusData.byVersion;
const byVersionsPrefix = `${issuesByStatusCount2Prefix} / ${statusName} / По версиям`;
if (byVersions) {
Object.keys(byVersions).forEach((versionName) => {
otherData.push({
title: `${byVersionsPrefix} / ${versionName} / Количество задач`,
value: byVersions[versionName].count,
details: {
issueIds: byVersions[versionName].issueIds,
},
});
});
}
}
});
// ШАГ 2. Количество задач по версиям
const issuesByVersionCount2 =
props.store?.data?.issuesMetrics?.issuesByVersionsCount ?? {};
const issuesByVersionCount2Prefix = 'Счётчики по версиям';
Object.keys(issuesByVersionCount2).forEach((versionName) => {
const versionData = issuesByVersionCount2[versionName];
if (versionData) {
otherData.push({
title: `${issuesByVersionCount2Prefix} / ${versionName} / Количество задач`,
value: versionData.count,
details: {
issueIds: versionData.issueIds,
},
});
const byStatuses = versionData.byStatus;
const byStatusesPrefix = `${issuesByVersionCount2Prefix} / ${versionName} / По статусам`;
if (byStatuses) {
Object.keys(byStatuses).forEach((statusName) => {
otherData.push({
title: `${byStatusesPrefix} / ${statusName} / Количество задач`,
value: byStatuses[statusName].count,
details: {
issueIds: byStatuses[statusName].issueIds,
},
});
});
}
}
});
// ШАГ 3. Количество задач по работникам
const issuesByUsername2 =
props.store?.data?.issuesMetrics?.issuesByUsername ?? {};
const issuesByUsername2Prefix = 'Счётчики по работникам';
Object.keys(issuesByUsername2).forEach((username) => {
const userData = issuesByUsername2[username];
if (userData) {
otherData.push({
title: `${issuesByUsername2Prefix} / ${username} / Количество задач`,
value: userData.count,
details: {
issueIds: userData.issueIds,
},
});
}
});
// ШАГ 4. Количество внутренних изменений
const changesCount2 = props.store?.data?.issuesMetrics?.changesCount ?? {};
const changesCount2Prefix = 'Счётчики изменений';
otherData.push({
title: `${changesCount2Prefix} / Всего изменений`,
value: changesCount2.totalChanges || 0,
});
otherData.push({
title: `${changesCount2Prefix} / Всего комментариев`,
value: changesCount2.totalComments || 0,
});
const sections: Record<string, string> = {
byIssue: 'По задачам',
byStatus: 'По статусам',
byVersion: 'По версиям',
byUserName: 'По работникам',
};
Object.keys(sections).forEach((sectionName) => {
const sectionData = changesCount2[sectionName];
if (!sectionData) return;
const sectionPrefix = `${changesCount2Prefix} / ${sections[sectionName]}`;
Object.keys(sectionData).forEach((itemKey) => {
const itemData = sectionData[itemKey];
otherData.push({
title: `${sectionPrefix} / ${itemKey} / Изменения`,
value: itemData.changes || 0,
});
otherData.push({
title: `${sectionPrefix} / ${itemKey} / Комментарии`,
value: itemData.comments || 0,
});
});
});
// ШАГ 5. Количество задач с оценками трудозатрат
const issuesWithEstimatesAndSpenthoursCount =
props.store?.data?.issuesMetrics?.issuesWithEstimatesAndSpenthoursCount ??
{};
const estimatesPrefix =
'Счётчики с оценкой трудозатрат и потраченных часов';
if (issuesWithEstimatesAndSpenthoursCount.withEstimates) {
otherData.push({
title: `${estimatesPrefix} / С оценкой трудозатрат`,
value: issuesWithEstimatesAndSpenthoursCount.withEstimates.count,
details: {
issueIds:
issuesWithEstimatesAndSpenthoursCount.withEstimates.issueIds,
},
});
}
if (issuesWithEstimatesAndSpenthoursCount.withoutEstimates) {
otherData.push({
title: `${estimatesPrefix} / Без оценки трудозатрат`,
value: issuesWithEstimatesAndSpenthoursCount.withoutEstimates.count,
details: {
issueIds:
issuesWithEstimatesAndSpenthoursCount.withoutEstimates.issueIds,
},
});
}
if (issuesWithEstimatesAndSpenthoursCount.withSpentHoursOverEstimates) {
otherData.push({
title: `${estimatesPrefix} / С потраченными часами больше, чем оценка трудозатрат`,
value:
issuesWithEstimatesAndSpenthoursCount.withSpentHoursOverEstimates
.count,
details: {
issueIds:
issuesWithEstimatesAndSpenthoursCount.withSpentHoursOverEstimates
.issueIds,
},
});
}
if (issuesWithEstimatesAndSpenthoursCount.byVersion) {
const estimatesPrefixVersions = `${estimatesPrefix} / По версиям`;
Object.keys(issuesWithEstimatesAndSpenthoursCount.byVersion).forEach(
(versionName) => {
const estimatesPrefixVersion = `${estimatesPrefixVersions} / ${versionName}`;
const versionData =
issuesWithEstimatesAndSpenthoursCount.byVersion[versionName];
if (versionData.withEstimates) {
otherData.push({
title: `${estimatesPrefixVersion} / С оценкой трудозатрат`,
value: versionData.withEstimates.count,
details: { issueIds: versionData.withEstimates.issueIds },
});
}
if (versionData.withoutEstimates) {
otherData.push({
title: `${estimatesPrefixVersion} / Без оценки трудозатрат`,
value: versionData.withoutEstimates.count,
details: { issueIds: versionData.withoutEstimates.issueIds },
});
}
if (versionData.withSpentHoursOverEstimates) {
otherData.push({
title: `${estimatesPrefixVersion} / С потраченными часами больше, чем оценка трудозатрат`,
value: versionData.withSpentHoursOverEstimates.count,
details: {
issueIds: versionData.withSpentHoursOverEstimates.issueIds,
},
});
}
},
);
}
if (issuesWithEstimatesAndSpenthoursCount.byStatus) {
const estimatesPrefixStatuses = `${estimatesPrefix} / По статусам`;
Object.keys(issuesWithEstimatesAndSpenthoursCount.byStatus).forEach(
(statusName) => {
const estimatesPrefixStatus = `${estimatesPrefixStatuses} / ${statusName}`;
const statusData =
issuesWithEstimatesAndSpenthoursCount.byStatus[statusName];
if (statusData.withEstimates) {
otherData.push({
title: `${estimatesPrefixStatus} / С оценкой трудозатрат`,
value: statusData.withEstimates.count,
details: { issueIds: statusData.withEstimates.issueIds },
});
}
if (statusData.withoutEstimates) {
otherData.push({
title: `${estimatesPrefixStatus} / Без оценки трудозатрат`,
value: statusData.withoutEstimates.count,
details: { issueIds: statusData.withoutEstimates.issueIds },
});
}
if (statusData.withSpentHoursOverEstimates) {
otherData.push({
title: `${estimatesPrefixStatus} / С потраченными часами больше, чем оценка трудозатрат`,
value: statusData.withSpentHoursOverEstimates.count,
details: {
issueIds: statusData.withSpentHoursOverEstimates.issueIds,
},
});
}
},
);
}
const byStatusLi: JSX.Element[] = []; const byStatusLi: JSX.Element[] = [];
@ -90,7 +455,8 @@ export const DailyEccmV2 = observer(
byStatusLi.push( byStatusLi.push(
<li key={`${keyPrefixForCurrentStatusBlock}-li`}> <li key={`${keyPrefixForCurrentStatusBlock}-li`}>
<SimpleCounterViewComponent <SimpleCounterViewComponent
key={`${keyPrefixForCurrentStatusBlock}`} key={keyPrefixForCurrentStatusBlock}
id={keyPrefixForCurrentStatusBlock}
label={statusName} label={statusName}
count={byStatusData.count} count={byStatusData.count}
issueIds={byStatusData.issueIds} issueIds={byStatusData.issueIds}
@ -110,6 +476,7 @@ export const DailyEccmV2 = observer(
<li key={`${keyPrefixForCurrentVersionBlock}-li`}> <li key={`${keyPrefixForCurrentVersionBlock}-li`}>
<SimpleCounterViewComponent <SimpleCounterViewComponent
key={keyPrefixForCurrentVersionBlock} key={keyPrefixForCurrentVersionBlock}
id={keyPrefixForCurrentVersionBlock}
label={versionName} label={versionName}
count={byVersionData.count} count={byVersionData.count}
issueIds={byVersionData.issueIds} issueIds={byVersionData.issueIds}
@ -129,6 +496,7 @@ export const DailyEccmV2 = observer(
<li key={`${keyPrefixForCurrentUserBlock}-li`}> <li key={`${keyPrefixForCurrentUserBlock}-li`}>
<SimpleCounterViewComponent <SimpleCounterViewComponent
key={keyPrefixForCurrentUserBlock} key={keyPrefixForCurrentUserBlock}
id={keyPrefixForCurrentUserBlock}
label={username} label={username}
count={byUserNameData.count} count={byUserNameData.count}
issueIds={byUserNameData.issueIds} issueIds={byUserNameData.issueIds}
@ -160,6 +528,22 @@ export const DailyEccmV2 = observer(
</li> </li>
</ul> </ul>
</li> </li>
<li>
По изменениям в задачах за период:
<ChangesCounterTotalViewComponent
key={`${keyPrefix}-changesCounterTotal`}
id={`${keyPrefix}-changesCounterTotal`}
data={changesCount}
/>
</li>
<li>
Другие счётчики:
<VerySimpleCounterListViewComponent
key={`${keyPrefix}-verySimpleCounterListViewComponent`}
id={`${keyPrefix}-verySimpleCounterListViewComponent`}
data={otherData || []}
/>
</li>
</ul> </ul>
<DebugInfo value={JSON.stringify(props.store?.data, null, 2)} /> <DebugInfo value={JSON.stringify(props.store?.data, null, 2)} />
</div> </div>

View file

@ -0,0 +1,55 @@
export function transformUtf8ToTransliteAscii(inputText: string): string {
// Define a mapping of Russian characters to their translite ASCII equivalents
const transliterationMap: { [key: string]: string } = {
а: 'a',
б: 'b',
в: 'v',
г: 'g',
д: 'd',
е: 'e',
ё: 'e',
ж: 'zh',
з: 'z',
и: 'i',
й: 'y',
к: 'k',
л: 'l',
м: 'm',
н: 'n',
о: 'o',
п: 'p',
р: 'r',
с: 's',
т: 't',
у: 'u',
ф: 'f',
х: 'kh',
ц: 'ts',
ч: 'ch',
ш: 'sh',
щ: 'sch',
ъ: '',
ы: 'y',
ь: '',
э: 'e',
ю: 'yu',
я: 'ya',
// Add more mappings as needed...
' ': '-',
'/': 'then',
};
// Transform the input text to translite ASCII without spaces
let transformedText = '';
for (const char of inputText.toLowerCase()) {
if (transliterationMap[char]) {
transformedText += transliterationMap[char];
} else {
transformedText += char;
}
}
return transformedText;
}
export const text2id = transformUtf8ToTransliteAscii;

View file

@ -314,7 +314,9 @@ export class DailyEccmV2ReportTaskRunnerService {
comments: 0, comments: 0,
}, },
); );
if (changes && (changes.changes > 0 || changes.comments > 0)) {
changesCount.byIssue[issue.id] = changes; changesCount.byIssue[issue.id] = changes;
}
}); });
} }
this.logger.debug( this.logger.debug(