diff --git a/src/backend/InvenTree/InvenTree/static/css/inventree.css b/src/backend/InvenTree/InvenTree/static/css/inventree.css index 912f48224375..f4ef9aae6c1f 100644 --- a/src/backend/InvenTree/InvenTree/static/css/inventree.css +++ b/src/backend/InvenTree/InvenTree/static/css/inventree.css @@ -1101,3 +1101,8 @@ a { .large-treeview-icon { font-size: 1em; } + +.test-statistics-table-total-row { + font-weight: bold; + border-top-style: double; +} diff --git a/src/backend/InvenTree/build/templates/build/build_base.html b/src/backend/InvenTree/build/templates/build/build_base.html index 4ead85bca821..2bebba0338f0 100644 --- a/src/backend/InvenTree/build/templates/build/build_base.html +++ b/src/backend/InvenTree/build/templates/build/build_base.html @@ -302,6 +302,12 @@

build: {{ build.pk }}, }); }); + + {% if build.part.trackable > 0 %} + onPanelLoad("test-statistics", function() { + prepareTestStatisticsTable('build', '{% url "api-stock-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 3d2e5d4e964f..39e12a2bffd7 100644 --- a/src/backend/InvenTree/build/templates/build/detail.html +++ b/src/backend/InvenTree/build/templates/build/detail.html @@ -255,6 +255,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 c038b7782a2e..de164067b6be 100644 --- a/src/backend/InvenTree/build/templates/build/sidebar.html +++ b/src/backend/InvenTree/build/templates/build/sidebar.html @@ -16,6 +16,10 @@ {% include "sidebar_item.html" with label='consumed' text=text icon="fa-list" %} {% 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 4cf36dc387bf..492fec32305c 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 Templates" %}

+
+
+
+

{% trans "Part Test Statistics" %}

+ {% include "spacer.html" %} +
+
+
+
+ {% include "filter_list.html" with id="partteststatistics" %} +
+ + {% include "test_statistics_table.html" with prefix="part-" %} +
+
+
@@ -754,6 +770,9 @@

{% trans "Part Manufacturers" %}

}); }); }); + onPanelLoad("test-statistics", function() { + prepareTestStatisticsTable('part', '{% url "api-stock-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 368110d7294a..8579cbd5cce9 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 f2b26743bcc2..952c11ef5ed7 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -12,8 +12,9 @@ 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 status +from rest_framework.generics import GenericAPIView from rest_framework.response import Response from rest_framework.serializers import ValidationError @@ -1308,6 +1309,51 @@ def filter_test_name(self, queryset, name, value): 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 downs by test result.""" + instance = self.get_object() + serializer = self.get_serializer(instance) + serializer.context['type'] = kwargs['type'] + 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.""" @@ -1672,6 +1718,22 @@ def destroy(self, request, *args, **kwargs): ), ]), ), + # Test statistics endpoints + path( + 'test-statistics/', + include([ + path( + '/', + include([ + path( + '/', + TestStatistics.as_view(), + name='api-stock-test-statistics', + ) + ]), + ) + ]), + ), # StockItemTracking API endpoints path( 'track/', diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 2d5cfaeaff09..614327b5bf20 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -33,6 +33,7 @@ import InvenTree.tasks import label.models import report.models +from build import models as BuildModels from company import models as CompanyModels from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField from InvenTree.status_codes import ( @@ -43,6 +44,7 @@ ) from part import models as PartModels from plugin.events import trigger_event +from stock import models as StockModels from users.models import Owner logger = logging.getLogger('inventree') @@ -2410,6 +2412,67 @@ def key(self): """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 a820de9ccc68..534a0a114eaa 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -676,6 +676,36 @@ def save(self): 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/part.js b/src/backend/InvenTree/templates/js/translated/part.js index ffcea223d372..f16f81239993 100644 --- a/src/backend/InvenTree/templates/js/translated/part.js +++ b/src/backend/InvenTree/templates/js/translated/part.js @@ -2976,7 +2976,6 @@ function loadPartTestTemplateTable(table, options) { }); } - /* * Load a chart which displays projected scheduling information for a particular part. * This takes into account: diff --git a/src/backend/InvenTree/templates/js/translated/stock.js b/src/backend/InvenTree/templates/js/translated/stock.js index e88f0602bbdc..5050d21bbd94 100644 --- a/src/backend/InvenTree/templates/js/translated/stock.js +++ b/src/backend/InvenTree/templates/js/translated/stock.js @@ -36,6 +36,7 @@ makeIconBadge, makeIconButton, makeRemoveButton, + moment, orderParts, partDetail, renderClipboard, @@ -68,6 +69,7 @@ duplicateStockItem, editStockItem, editStockLocation, + filterTestStatisticsTableDateRange, findStockItemBySerialNumber, installStockItem, loadInstalledInTable, @@ -76,6 +78,7 @@ loadStockTestResultsTable, loadStockTrackingTable, loadTableFilters, + prepareTestStatisticsTable, mergeStockItems, removeStockRow, serializeStockItem, @@ -3337,3 +3340,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'] + header = ''; + rows = [] + passed= ''; + failed = ''; + 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') { + 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"), 'buildteststatistics', options); + } + }, + { + icon: 'fa-calendar-week', + title: '{% trans "This month" %}', + label: 'this-month', + callback: function(data) { + filterTestStatisticsTableDateRange(data, 'this-month', $("#test-statistics-table"), 'buildteststatistics', options); + } + }, + ], + } + ], + callback: function(table, filters, options) { + loadTestStatisticsTable($("#test-statistics-table"), keyName, apiUrl, options, filters); + } + } + setupFilterList('buildteststatistics', $("#test-statistics-table"), '#filter-list-buildteststatistics', 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 0c10b06f3325..2447c1289207 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() { @@ -858,6 +872,8 @@ function getAvailableTableFilters(tableKey) { return getBuildItemTableFilters(); case 'buildlines': return getBuildLineTableFilters(); + case 'buildteststatistics': + return getTestStatisticsTableFilters(); case 'bom': return getBOMTableFilters(); case 'category': @@ -880,6 +896,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 000000000000..24361a3bf932 --- /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 e8e5ebc844fd..48ef56918ae8 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -106,6 +106,8 @@ export enum ApiEndpoints { stock_assign = 'stock/assign/', stock_status = 'stock/status/', stock_install = 'stock/:id/install', + build_test_statistics = 'stock/test-statistics/by-build/:id', + part_test_statistics = 'stock/test-statistics/by-part/:id', // Order API endpoints purchase_order_list = 'order/po/', diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index fbbffa5e8199..c5407f95ee33 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -12,6 +12,7 @@ import { IconPaperclip, IconPrinter, IconQrcode, + IconReportAnalytics, IconSitemap } from '@tabler/icons-react'; import { useMemo } from 'react'; @@ -49,6 +50,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 @@ -283,6 +285,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 d72907186b7a..7e8e6fb498d0 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, @@ -77,6 +78,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'; /** @@ -590,6 +592,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 000000000000..c7706ec0934f --- /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 ( + + ); +}