diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index c0e3e67f6f..4161388461 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 229 +INVENTREE_API_VERSION = 230 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v230 - 2024-05-05 : https://github.com/inventree/InvenTree/pull/7164 + - Adds test statistics endpoint + v229 - 2024-07-31 : https://github.com/inventree/InvenTree/pull/7775 - Add extra exportable fields to the BomItem serializer diff --git a/src/backend/InvenTree/InvenTree/static/css/inventree.css b/src/backend/InvenTree/InvenTree/static/css/inventree.css index cc392d28b7..185ca70e8c 100644 --- a/src/backend/InvenTree/InvenTree/static/css/inventree.css +++ b/src/backend/InvenTree/InvenTree/static/css/inventree.css @@ -1112,3 +1112,8 @@ a { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + +.test-statistics-table-total-row { + font-weight: bold; + border-top-style: double; +} diff --git a/src/backend/InvenTree/InvenTree/urls.py b/src/backend/InvenTree/InvenTree/urls.py index 4d3d7f95c6..91a6ce35ca 100644 --- a/src/backend/InvenTree/InvenTree/urls.py +++ b/src/backend/InvenTree/InvenTree/urls.py @@ -35,6 +35,7 @@ from company.urls import company_urls, manufacturer_part_urls, supplier_part_url from order.urls import order_urls from part.urls import part_urls from plugin.urls import get_plugin_urls +from stock.api import test_statistics_api_urls from stock.urls import stock_urls from web.urls import api_urls as web_api_urls from web.urls import urlpatterns as platform_urls @@ -109,6 +110,7 @@ apipatterns = [ ), ]), ), + path('test-statistics/', include(test_statistics_api_urls)), path('user/', include(users.api.user_urls)), path('web/', include(web_api_urls)), # Plugin endpoints diff --git a/src/backend/InvenTree/build/templates/build/build_base.html b/src/backend/InvenTree/build/templates/build/build_base.html index 5a40b32e7b..3223c7b3d8 100644 --- a/src/backend/InvenTree/build/templates/build/build_base.html +++ b/src/backend/InvenTree/build/templates/build/build_base.html @@ -298,6 +298,12 @@ src="{% static 'img/blank_image.png' %}" build: {{ build.pk }}, }); }); + + {% if build.part.trackable > 0 %} + onPanelLoad("test-statistics", function() { + prepareTestStatisticsTable('build', '{% url "api-test-statistics-by-build" build.pk %}') + }); + {% endif %} {% endif %} {% endif %} diff --git a/src/backend/InvenTree/build/templates/build/detail.html b/src/backend/InvenTree/build/templates/build/detail.html index 6cabe1f01c..e1b2c47c61 100644 --- a/src/backend/InvenTree/build/templates/build/detail.html +++ b/src/backend/InvenTree/build/templates/build/detail.html @@ -267,6 +267,21 @@ +
+
+

+ {% trans "Build test statistics" %} +

+
+ +
+
+ {% include "filter_list.html" with id="buildteststatistics" %} +
+ {% include "test_statistics_table.html" with prefix="build-" %} +
+
+
diff --git a/src/backend/InvenTree/build/templates/build/sidebar.html b/src/backend/InvenTree/build/templates/build/sidebar.html index 1c20429f2f..ef3ca6f14d 100644 --- a/src/backend/InvenTree/build/templates/build/sidebar.html +++ b/src/backend/InvenTree/build/templates/build/sidebar.html @@ -20,6 +20,10 @@ {% include "sidebar_item.html" with label='consumed' text=text icon="fa-tasks" %} {% trans "Child Build Orders" as text %} {% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %} +{% if build.part.trackable %} +{% trans "Test Statistics" as text %} +{% include "sidebar_item.html" with label='test-statistics' text=text icon="fa-chart-line" %} +{% endif %} {% trans "Attachments" as text %} {% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %} {% trans "Notes" as text %} diff --git a/src/backend/InvenTree/part/templates/part/detail.html b/src/backend/InvenTree/part/templates/part/detail.html index 7aa0458c1b..eaec60b375 100644 --- a/src/backend/InvenTree/part/templates/part/detail.html +++ b/src/backend/InvenTree/part/templates/part/detail.html @@ -100,6 +100,22 @@
+
+
+
+

{% trans "Part Test Statistics" %}

+ {% include "spacer.html" %} +
+
+
+
+ {% include "filter_list.html" with id="partteststatistics" %} +
+ + {% include "test_statistics_table.html" with prefix="part-" %} +
+
+
@@ -751,6 +767,9 @@ }); }); }); + onPanelLoad("test-statistics", function() { + prepareTestStatisticsTable('part', '{% url "api-test-statistics-by-part" part.pk %}') + }); onPanelLoad("part-stock", function() { $('#new-stock-item').click(function () { diff --git a/src/backend/InvenTree/part/templates/part/part_sidebar.html b/src/backend/InvenTree/part/templates/part/part_sidebar.html index 368110d729..8579cbd5cc 100644 --- a/src/backend/InvenTree/part/templates/part/part_sidebar.html +++ b/src/backend/InvenTree/part/templates/part/part_sidebar.html @@ -53,6 +53,8 @@ {% if part.trackable %} {% trans "Test Templates" as text %} {% include "sidebar_item.html" with label="test-templates" text=text icon="fa-vial" %} +{% trans "Test Statistics" as text %} +{% include "sidebar_item.html" with label="test-statistics" text=text icon="fa-chart-line" %} {% endif %} {% if show_related %} {% trans "Related Parts" as text %} diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 4ea5be3117..00119d1486 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _ from django_filters import rest_framework as rest_filters from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema_field +from drf_spectacular.utils import extend_schema, extend_schema_field from rest_framework import permissions, status from rest_framework.generics import GenericAPIView from rest_framework.response import Response @@ -1288,6 +1288,54 @@ class StockItemTestResultFilter(rest_filters.FilterSet): return queryset.filter(template__key=key) +class TestStatisticsFilter(rest_filters.FilterSet): + """API filter for the filtering the test results belonging to a specific build.""" + + class Meta: + """Metaclass options.""" + + model = StockItemTestResult + fields = [] + + # Created date filters + finished_before = InvenTreeDateFilter( + label='Finished before', field_name='finished_datetime', lookup_expr='lte' + ) + finished_after = InvenTreeDateFilter( + label='Finished after', field_name='finished_datetime', lookup_expr='gte' + ) + + +class TestStatistics(GenericAPIView): + """API endpoint for accessing a test statistics broken down by test templates.""" + + queryset = StockItemTestResult.objects.all() + serializer_class = StockSerializers.TestStatisticsSerializer + pagination_class = None + filterset_class = TestStatisticsFilter + filter_backends = SEARCH_ORDER_FILTER_ALIAS + + @extend_schema( + responses={200: StockSerializers.TestStatisticsSerializer(many=False)} + ) + def get(self, request, pk, *args, **kwargs): + """Return test execution count matrix broken down by test result.""" + instance = self.get_object() + serializer = self.get_serializer(instance) + if request.resolver_match.url_name == 'api-test-statistics-by-part': + serializer.context['type'] = 'by-part' + elif request.resolver_match.url_name == 'api-test-statistics-by-build': + serializer.context['type'] = 'by-build' + serializer.context['finished_datetime_after'] = self.request.query_params.get( + 'finished_datetime_after' + ) + serializer.context['finished_datetime_before'] = self.request.query_params.get( + 'finished_datetime_before' + ) + serializer.context['pk'] = pk + return Response([serializer.data]) + + class StockItemTestResultList(StockItemTestResultMixin, ListCreateDestroyAPIView): """API endpoint for listing (and creating) a StockItemTestResult object.""" @@ -1663,3 +1711,27 @@ stock_api_urls = [ # Anything else path('', StockList.as_view(), name='api-stock-list'), ] + +test_statistics_api_urls = [ + # Test statistics endpoints + path( + 'by-part/', + include([ + path( + '/', + TestStatistics.as_view(), + name='api-test-statistics-by-part', + ) + ]), + ), + path( + 'by-build/', + include([ + path( + '/', + TestStatistics.as_view(), + name='api-test-statistics-by-build', + ) + ]), + ), +] diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 936c7097ce..35ce61a99a 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -33,6 +33,7 @@ import InvenTree.ready import InvenTree.tasks import report.mixins import report.models +from build import models as BuildModels from common.icons import validate_icon from common.settings import get_global_setting from company import models as CompanyModels @@ -40,6 +41,7 @@ from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField from order.status_codes import SalesOrderStatusGroups from part import models as PartModels from plugin.events import trigger_event +from stock import models as StockModels from stock.generators import generate_batch_code from stock.status_codes import StockHistoryCode, StockStatus, StockStatusGroups from users.models import Owner @@ -2459,6 +2461,67 @@ class StockItemTestResult(InvenTree.models.InvenTreeMetadataModel): """Return key for test.""" return InvenTree.helpers.generateTestKey(self.test_name) + def calculate_test_statistics_for_test_template( + self, query_base, test_template, ret, start, end + ): + """Helper function to calculate the passed/failed/total tests count per test template type.""" + query = query_base & Q(template=test_template.pk) + if start is not None and end is not None: + query = query & Q(started_datetime__range=(start, end)) + elif start is not None and end is None: + query = query & Q(started_datetime__gt=start) + elif start is None and end is not None: + query = query & Q(started_datetime__lt=end) + + passed = StockModels.StockItemTestResult.objects.filter( + query & Q(result=True) + ).count() + failed = StockModels.StockItemTestResult.objects.filter( + query & ~Q(result=True) + ).count() + if test_template.test_name not in ret: + ret[test_template.test_name] = {'passed': 0, 'failed': 0, 'total': 0} + ret[test_template.test_name]['passed'] += passed + ret[test_template.test_name]['failed'] += failed + ret[test_template.test_name]['total'] += passed + failed + ret['total']['passed'] += passed + ret['total']['failed'] += failed + ret['total']['total'] += passed + failed + return ret + + def build_test_statistics(self, build_order_pk, start, end): + """Generate a statistics matrix for each test template based on the test executions result counts.""" + build = BuildModels.Build.objects.get(pk=build_order_pk) + if not build or not build.part.trackable: + return {} + + test_templates = build.part.getTestTemplates() + ret = {'total': {'passed': 0, 'failed': 0, 'total': 0}} + for build_item in build.get_build_outputs(): + for test_template in test_templates: + query_base = Q(stock_item=build_item) + ret = self.calculate_test_statistics_for_test_template( + query_base, test_template, ret, start, end + ) + return ret + + def part_test_statistics(self, part_pk, start, end): + """Generate a statistics matrix for each test template based on the test executions result counts.""" + part = PartModels.Part.objects.get(pk=part_pk) + + if not part or not part.trackable: + return {} + + test_templates = part.getTestTemplates() + ret = {'total': {'passed': 0, 'failed': 0, 'total': 0}} + for bo in part.stock_entries(): + for test_template in test_templates: + query_base = Q(stock_item=bo) + ret = self.calculate_test_statistics_for_test_template( + query_base, test_template, ret, start, end + ) + return ret + stock_item = models.ForeignKey( StockItem, on_delete=models.CASCADE, related_name='test_results' ) diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 8cbdd28566..e92c133eb7 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -870,6 +870,36 @@ class UninstallStockItemSerializer(serializers.Serializer): item.uninstall_into_location(location, request.user, note) +class TestStatisticsLineField(serializers.DictField): + """DRF field definition for one column of the test statistics.""" + + test_name = serializers.CharField() + results = serializers.DictField(child=serializers.IntegerField(min_value=0)) + + +class TestStatisticsSerializer(serializers.Serializer): + """DRF serializer class for the test statistics.""" + + results = serializers.ListField(child=TestStatisticsLineField(), read_only=True) + + def to_representation(self, obj): + """Just pass through the test statistics from the model.""" + if self.context['type'] == 'by-part': + return obj.part_test_statistics( + self.context['pk'], + self.context['finished_datetime_after'], + self.context['finished_datetime_before'], + ) + elif self.context['type'] == 'by-build': + return obj.build_test_statistics( + self.context['pk'], + self.context['finished_datetime_after'], + self.context['finished_datetime_before'], + ) + + raise ValidationError(_('Unsupported statistic type: ' + self.context['type'])) + + class ConvertStockItemSerializer(serializers.Serializer): """DRF serializer class for converting a StockItem to a valid variant part.""" diff --git a/src/backend/InvenTree/templates/js/translated/filters.js b/src/backend/InvenTree/templates/js/translated/filters.js index a6ad9e505c..3e6dfc8220 100644 --- a/src/backend/InvenTree/templates/js/translated/filters.js +++ b/src/backend/InvenTree/templates/js/translated/filters.js @@ -15,6 +15,9 @@ /* exported setupFilterList, + addTableFilter, + removeTableFilter, + reloadTableFilters */ /** diff --git a/src/backend/InvenTree/templates/js/translated/stock.js b/src/backend/InvenTree/templates/js/translated/stock.js index fae4919ffb..f9c5c2b3e3 100644 --- a/src/backend/InvenTree/templates/js/translated/stock.js +++ b/src/backend/InvenTree/templates/js/translated/stock.js @@ -4,6 +4,7 @@ /* globals addCachedAlert, + addTableFilter, baseCurrency, calculateTotalPrice, clearFormInput, @@ -38,8 +39,11 @@ makeIconBadge, makeIconButton, makeRemoveButton, + moment, orderParts, partDetail, + reloadTableFilters, + removeTableFilter, renderClipboard, renderDate, renderLink, @@ -70,6 +74,7 @@ duplicateStockItem, editStockItem, editStockLocation, + filterTestStatisticsTableDateRange, findStockItemBySerialNumber, installStockItem, loadInstalledInTable, @@ -78,6 +83,7 @@ loadStockTestResultsTable, loadStockTrackingTable, loadTableFilters, + prepareTestStatisticsTable, mergeStockItems, removeStockRow, serializeStockItem, @@ -3411,3 +3417,105 @@ function setStockStatus(items, options={}) { } }); } + + +/* + * Load TestStatistics table. + */ +function loadTestStatisticsTable(table, prefix, url, options, filters = {}) { + inventreeGet(url, filters, { + async: true, + success: function(data) { + const keys = ['passed', 'failed', 'total'] + let header = ''; + let rows = [] + let passed= ''; + let failed = ''; + let total = ''; + $('.test-stat-result-cell').remove(); + $.each(data[0], function(key, value){ + if (key != "total") { + header += '' + key + ''; + keys.forEach(function(keyName) { + var tdText = '-'; + if (value['total'] != '0' && value[keyName] != '0') { + let percentage = '' + if (keyName != 'total' && value[total] != 0) { + percentage = ' (' + (100.0 * (parseFloat(value[keyName]) / parseFloat(value['total']))).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 2}) + '%)'; + } + tdText = value[keyName] + percentage; + } + rows[keyName] += '' + tdText + ''; + }) + } + }); + $('#' + prefix + '-test-statistics-table-header-id').after(header); + + keys.forEach(function(keyName) { + let valueStr = data[0]['total'][keyName]; + if (keyName != 'total' && data[0]['total']['total'] != '0') { + valueStr += ' (' + (100.0 * (parseFloat(valueStr) / parseFloat(data[0]['total']['total']))).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 2}) + '%)'; + } + rows[keyName] += '' + valueStr + ''; + $('#' + prefix + '-test-statistics-table-body-' + keyName).after(rows[keyName]); + }); + $('#' + prefix + '-test-statistics-table').show(); + setupFilterList(prefix + "teststatistics", table, "#filter-list-" + prefix + "teststatistics", options); + }, + }); +} + +function prepareTestStatisticsTable(keyName, apiUrl) +{ + let options = { + custom_actions: [ + { + icon: 'fa-calendar-week', + actions: [ + { + icon: 'fa-calendar-week', + title: '{% trans "This week" %}', + label: 'this-week', + callback: function(data) { + filterTestStatisticsTableDateRange(data, 'this-week', $("#test-statistics-table"), keyName + 'teststatistics', options); + } + }, + { + icon: 'fa-calendar-week', + title: '{% trans "This month" %}', + label: 'this-month', + callback: function(data) { + filterTestStatisticsTableDateRange(data, 'this-month', $("#test-statistics-table"), keyName + 'teststatistics', options); + } + }, + ], + } + ], + callback: function(table, filters, options) { + loadTestStatisticsTable($("#test-statistics-table"), keyName, apiUrl, options, filters); + } + } + setupFilterList(keyName + 'teststatistics', $("#test-statistics-table"), '#filter-list-' + keyName + 'teststatistics', options); + + // Load test statistics table + loadTestStatisticsTable($("#test-statistics-table"), keyName, apiUrl, options); +} + +function filterTestStatisticsTableDateRange(data, range, table, tableKey, options) +{ + var startDateString = ''; + var d = new Date(); + if (range == "this-week") { + startDateString = moment(new Date(d.getFullYear(), d.getMonth(), d.getDate() - (d.getDay() == 0 ? 6 : d.getDay() - 1))).format('YYYY-MM-DD'); + } else if (range == "this-month") { + startDateString = moment(new Date(d.getFullYear(), d.getMonth(), 1)).format('YYYY-MM-DD'); + } else { + console.warn(`Invalid range specified for filterTestStatisticsTableDateRange`); + return; + } + var filters = addTableFilter(tableKey, 'finished_datetime_after', startDateString); + removeTableFilter(tableKey, 'finished_datetime_before') + + reloadTableFilters(table, filters, options); + setupFilterList(tableKey, table, "#filter-list-" + tableKey, options); +} diff --git a/src/backend/InvenTree/templates/js/translated/table_filters.js b/src/backend/InvenTree/templates/js/translated/table_filters.js index c32b69f8b6..71b8c8a917 100644 --- a/src/backend/InvenTree/templates/js/translated/table_filters.js +++ b/src/backend/InvenTree/templates/js/translated/table_filters.js @@ -462,6 +462,20 @@ function getStockTestTableFilters() { }; } +// Return a dictionary of filters for the "test statistics" table +function getTestStatisticsTableFilters() { + + return { + finished_datetime_after: { + type: 'date', + title: '{% trans "Interval start" %}', + }, + finished_datetime_before: { + type: 'date', + title: '{% trans "Interval end" %}', + } + }; +} // Return a dictionary of filters for the "stocktracking" table function getStockTrackingTableFilters() { @@ -863,6 +877,8 @@ function getAvailableTableFilters(tableKey) { return getBuildItemTableFilters(); case 'buildlines': return getBuildLineTableFilters(); + case 'buildteststatistics': + return getTestStatisticsTableFilters(); case 'bom': return getBOMTableFilters(); case 'category': @@ -885,6 +901,8 @@ function getAvailableTableFilters(tableKey) { return getPartTableFilters(); case 'parttests': return getPartTestTemplateFilters(); + case 'partteststatistics': + return getTestStatisticsTableFilters(); case 'plugins': return getPluginTableFilters(); case 'purchaseorder': diff --git a/src/backend/InvenTree/templates/test_statistics_table.html b/src/backend/InvenTree/templates/test_statistics_table.html new file mode 100644 index 0000000000..24361a3bf9 --- /dev/null +++ b/src/backend/InvenTree/templates/test_statistics_table.html @@ -0,0 +1,22 @@ +{% load i18n %} +{% load inventree_extras %} + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index e6a211eeff..62b54cdc27 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -115,6 +115,8 @@ export enum ApiEndpoints { stock_assign = 'stock/assign/', stock_status = 'stock/status/', stock_install = 'stock/:id/install', + build_test_statistics = 'test-statistics/by-build/:id', + part_test_statistics = 'test-statistics/by-part/:id', // Generator API endpoints generate_batch_code = 'generate/batch-code/', diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index b4caadabd6..ea10ca0baf 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -11,6 +11,7 @@ import { IconNotes, IconPaperclip, IconQrcode, + IconReportAnalytics, IconSitemap } from '@tabler/icons-react'; import { useMemo } from 'react'; @@ -53,6 +54,7 @@ import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import BuildOutputTable from '../../tables/build/BuildOutputTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; +import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable'; /** * Detail page for a single Build Order @@ -305,6 +307,20 @@ export default function BuildDetail() { ) }, + { + name: 'test-statistics', + label: t`Test Statistics`, + icon: , + content: ( + + ), + hidden: !build?.part_detail?.trackable + }, { name: 'attachments', label: t`Attachments`, diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index f6587d06a4..633ad3ae55 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -15,6 +15,7 @@ import { IconNotes, IconPackages, IconPaperclip, + IconReportAnalytics, IconShoppingCart, IconStack2, IconTestPipe, @@ -85,6 +86,7 @@ import { ManufacturerPartTable } from '../../tables/purchasing/ManufacturerPartT import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable'; import { SalesOrderTable } from '../../tables/sales/SalesOrderTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; +import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable'; import PartPricingPanel from './PartPricingPanel'; /** @@ -630,6 +632,22 @@ export default function PartDetail() { ) }, + { + name: 'test_statistics', + label: t`Test Statistics`, + icon: , + hidden: !part.trackable, + content: part?.pk ? ( + + ) : ( + + ) + }, { name: 'related_parts', label: t`Related Parts`, diff --git a/src/frontend/src/tables/stock/TestStatisticsTable.tsx b/src/frontend/src/tables/stock/TestStatisticsTable.tsx new file mode 100644 index 0000000000..c7706ec093 --- /dev/null +++ b/src/frontend/src/tables/stock/TestStatisticsTable.tsx @@ -0,0 +1,136 @@ +import { t } from '@lingui/macro'; +import { useCallback, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { AddItemButton } from '../../components/buttons/AddItemButton'; +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { UserRoles } from '../../enums/Roles'; +import { stockLocationFields } from '../../forms/StockForms'; +import { getDetailUrl } from '../../functions/urls'; +import { + useCreateApiFormModal, + useEditApiFormModal +} from '../../hooks/UseForm'; +import { useTable } from '../../hooks/UseTable'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; +import { TableColumn } from '../Column'; +import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers'; +import { TableFilter } from '../Filter'; +import { InvenTreeTable } from '../InvenTreeTable'; + +export function TestStatisticsTable({ params = {} }: { params?: any }) { + const initialColumns: TableColumn[] = []; + const [templateColumnList, setTemplateColumnList] = useState(initialColumns); + + const testTemplateColumns: TableColumn[] = useMemo(() => { + let data = templateColumnList ?? []; + return data; + }, [templateColumnList]); + + const tableColumns: TableColumn[] = useMemo(() => { + const firstColumn: TableColumn = { + accessor: 'col_0', + title: '', + sortable: true, + switchable: false, + noWrap: true + }; + + const lastColumn: TableColumn = { + accessor: 'col_total', + sortable: true, + switchable: false, + noWrap: true, + title: t`Total` + }; + + return [firstColumn, ...testTemplateColumns, lastColumn]; + }, [testTemplateColumns]); + + function statCountString(count: number, total: number) { + if (count > 0) { + let percentage = + ' (' + + ((100.0 * count) / total).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 2 + }) + + '%)'; + return count.toString() + percentage; + } + return '-'; + } + + // Format the test results based on the returned data + const formatRecords = useCallback((records: any[]): any[] => { + // interface needed to being able to dynamically assign keys + interface ResultRow { + [key: string]: string; + } + // Construct a list of test templates + let results: ResultRow[] = [ + { id: 'row_passed', col_0: t`Passed` }, + { id: 'row_failed', col_0: t`Failed` }, + { id: 'row_total', col_0: t`Total` } + ]; + let columnIndex = 0; + + columnIndex = 1; + + let newColumns: TableColumn[] = []; + for (let key in records[0]) { + if (key == 'total') continue; + let acc = 'col_' + columnIndex.toString(); + + const resultKeys = ['passed', 'failed', 'total']; + + results[0][acc] = statCountString( + records[0][key]['passed'], + records[0][key]['total'] + ); + results[1][acc] = statCountString( + records[0][key]['failed'], + records[0][key]['total'] + ); + results[2][acc] = records[0][key]['total'].toString(); + + newColumns.push({ + accessor: 'col_' + columnIndex.toString(), + title: key + }); + columnIndex++; + } + + setTemplateColumnList(newColumns); + + results[0]['col_total'] = statCountString( + records[0]['total']['passed'], + records[0]['total']['total'] + ); + results[1]['col_total'] = statCountString( + records[0]['total']['failed'], + records[0]['total']['total'] + ); + results[2]['col_total'] = records[0]['total']['total'].toString(); + + return results; + }, []); + + const table = useTable('teststatistics'); + + return ( + + ); +}