TableNG: Footer enhancements (#102948)
* Table: Footer support for multiple reducers * fix migrator test output due to required default value * fix go migration test * more go test fixes * fix go tests for footer * Merge #110062 * update migration dashboard * Small docs fixes * Small docs fix * remove migration method in Go, update js unit test * add migration dashboard for footer and clean up some issues with countAll * Footer should always use og rows for calcs * update footer to be unaffected by filtering and sorting * more e2es * add more complex footer to kitchen sink, migrate panel all the way up * update codeowners for e2e * relocate footer migration panels and e2es to main 12.2 migration dashboard * go further with the migration; kill unused fields, rename reducer to reducers * get the go code up to date, move enablePagination to its own option as well * add a unit to one of the numeric columns with a footer in kitchen sink * fix reducers override in kitchen sink migration table --------- Co-authored-by: Paul Marbach <paul.marbach@grafana.com> Co-authored-by: Isabel Matwawana <isabel.matwawana@grafana.com> Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
co-authored by
Paul Marbach
Isabel Matwawana
Isabel Matwawana
Leon Sorokin
parent
ae97eb860c
commit
6c9faa7595
+1
-6
@@ -939,13 +939,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/Table/TableNG/utils.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/Table/TableNG/utils.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/Table/TableRT/Filter.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
|
||||
@@ -462,6 +462,7 @@
|
||||
/e2e-playwright/panels-suite/panelEdit_base.spec.ts @grafana/dashboards-squad
|
||||
/e2e-playwright/panels-suite/panelEdit_queries.spec.ts @grafana/dashboards-squad
|
||||
/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts @grafana/datapro
|
||||
/e2e-playwright/panels-suite/table-footer.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/table-markdown.spec.ts @grafana/dataviz-squad
|
||||
/e2e-playwright/panels-suite/table-sparkline.spec.ts @grafana/dataviz-squad
|
||||
|
||||
@@ -511,6 +511,9 @@ func migrateDefaults(prevDefaults map[string]interface{}) map[string]interface{}
|
||||
"type": "auto",
|
||||
},
|
||||
"inspect": false,
|
||||
"footer": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ func TestV24(t *testing.T) {
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"footer": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
@@ -155,6 +158,9 @@ func TestV24(t *testing.T) {
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "color-background",
|
||||
},
|
||||
"footer": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
@@ -287,6 +293,9 @@ func TestV24(t *testing.T) {
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"footer": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
@@ -364,6 +373,9 @@ func TestV24(t *testing.T) {
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"footer": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
@@ -440,6 +452,9 @@ func TestV24(t *testing.T) {
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"footer": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
@@ -515,6 +530,9 @@ func TestV24(t *testing.T) {
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"footer": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
@@ -601,6 +619,9 @@ func TestV24(t *testing.T) {
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"footer": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
@@ -783,6 +804,9 @@ func TestV24(t *testing.T) {
|
||||
"cellOptions": map[string]interface{}{
|
||||
"type": "auto",
|
||||
},
|
||||
"footer": map[string]interface{}{
|
||||
"reducers": []interface{}{},
|
||||
},
|
||||
"inspect": false,
|
||||
},
|
||||
"mappings": []interface{}{},
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -87,4 +90,4 @@
|
||||
"refresh": "",
|
||||
"schemaVersion": 41,
|
||||
"title": "V22 Table Panel Styles Test"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -91,6 +94,9 @@
|
||||
"cellOptions": {
|
||||
"type": "color-background"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"decimals": 2,
|
||||
@@ -241,6 +247,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"decimals": 1,
|
||||
@@ -318,6 +327,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -386,6 +398,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -454,6 +469,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -521,6 +539,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -600,6 +621,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -669,6 +693,9 @@
|
||||
"cellOptions": {
|
||||
"type": "color-background"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -772,6 +799,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -869,6 +899,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -979,6 +1012,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
@@ -1095,6 +1131,9 @@
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"footer": {
|
||||
"reducers": []
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 21409,
|
||||
"id": 1827,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
@@ -33,11 +33,11 @@
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto",
|
||||
"wrapText": false
|
||||
"type": "auto"
|
||||
},
|
||||
"filterable": true,
|
||||
"inspect": false
|
||||
"inspect": false,
|
||||
"wrapText": false
|
||||
},
|
||||
"fieldMinMax": true,
|
||||
"links": [],
|
||||
@@ -81,6 +81,10 @@
|
||||
{
|
||||
"id": "custom.inspect",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"id": "custom.footer.reducers",
|
||||
"value": ["sum"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -154,6 +158,10 @@
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "dateTimeFromNow"
|
||||
},
|
||||
{
|
||||
"id": "custom.footer.reducers",
|
||||
"value": ["countAll"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -242,6 +250,10 @@
|
||||
"fixedColor": "blue",
|
||||
"mode": "continuous-YlRd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "areaM2"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -277,7 +289,7 @@
|
||||
"value": "State"
|
||||
},
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"id": "custom.hideFrom.viz",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
@@ -291,10 +303,13 @@
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"type": "auto",
|
||||
"wrapText": true
|
||||
"type": "auto"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.wrapText",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"id": "custom.width",
|
||||
"value": 255
|
||||
@@ -306,6 +321,10 @@
|
||||
{
|
||||
"id": "custom.tooltip.placement",
|
||||
"value": "left"
|
||||
},
|
||||
{
|
||||
"id": "custom.footer.reducers",
|
||||
"value": ["last"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -318,10 +337,13 @@
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"type": "pill",
|
||||
"wrapText": true
|
||||
"type": "pill"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.wrapText",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"id": "custom.width",
|
||||
"value": 120
|
||||
@@ -337,10 +359,13 @@
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"type": "data-links",
|
||||
"wrapText": true
|
||||
"type": "data-links"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.wrapText",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"id": "links",
|
||||
"value": [
|
||||
@@ -421,6 +446,21 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byNames",
|
||||
"options": {
|
||||
"mode": "include",
|
||||
"names": ["Min", "Max"]
|
||||
}
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.footer.reducers",
|
||||
"value": ["min", "max"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -433,13 +473,6 @@
|
||||
"id": 1,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"enablePagination": false,
|
||||
"fields": ["Min", "Max"],
|
||||
"reducer": ["max"],
|
||||
"show": true
|
||||
},
|
||||
"frameIndex": 0,
|
||||
"frozenColumns": {
|
||||
"left": 1
|
||||
@@ -452,7 +485,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -556,6 +589,9 @@
|
||||
"type": "color-background"
|
||||
},
|
||||
"filterable": true,
|
||||
"footer": {
|
||||
"reducer": []
|
||||
},
|
||||
"inspect": true
|
||||
},
|
||||
"fieldMinMax": true,
|
||||
@@ -587,13 +623,7 @@
|
||||
"maxDataPoints": 100,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"enablePagination": false,
|
||||
"fields": "",
|
||||
"reducer": ["max"],
|
||||
"show": false
|
||||
},
|
||||
"enablePagination": false,
|
||||
"showHeader": true,
|
||||
"sortBy": [
|
||||
{
|
||||
@@ -602,7 +632,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -812,16 +842,10 @@
|
||||
"id": 2,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": ["sum"],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "auto,link,links,bg,bg-link,bg-links,color,color-link,color-links,bg-row,bg-row-link,bg-row-links,color-row,color-row-link,color-row-links,links-cellType\na,a,a,a,a,a,a,a,a,1,1,1,1,1,1,1\nb,b,b,b,b,b,b,b,b,2,2,2,2,2,2,2\nc,c,c,c,c,c,c,c,c,3,3,3,3,3,3,3\nd,d,d,d,d,d,d,d,d,4,4,4,4,4,4,4",
|
||||
@@ -975,16 +999,10 @@
|
||||
"id": 10,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": ["sum"],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "use-case,normal,color-text,color-bg,highlight\n\"color bg and apply to row\",1,0,1,1\n\"color text and apply to row\",1,1,0,1\n\"color text + color bg + apply to row\",1,1,1,1\n\"only apply to row\",1,0,0,1\n\"only color bg\",1,0,1,0\n\"only color text\",1,1,0,0\n\"color text + color bg\",1,1,1,0\n\"no colorization\",1,0,0,0",
|
||||
@@ -1061,7 +1079,7 @@
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"id": "custom.hideFrom.viz",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
@@ -1124,15 +1142,9 @@
|
||||
"maxDataPoints": 10,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": ["sum"],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"Time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Min\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Max\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Info\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"State\",\n \"type\": \"enum\",\n \"typeInfo\": {\n \"frame\": \"enum\"\n },\n \"config\": {\n \"type\": {\n \"enum\": {\n \"text\": [\n \"Unknown\",\n \"Up\",\n \"Down\"\n ]\n }\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1754578636810,\n 1754580436810,\n 1754582236810,\n 1754584036810,\n 1754585836810,\n 1754587636810,\n 1754589436810,\n 1754591236810,\n 1754593036810,\n 1754594836810\n ],\n [\n 68.67815082171406,\n 68.21038738705283,\n 67.85748579518524,\n 67.48088827530566,\n 67.36362085363314,\n 67.75399477824531,\n 67.9006935145112,\n 68.2893033558627,\n 68.20547872017494,\n 67.96251158324088\n ],\n [\n 67.52655915076163,\n 68.03367017865995,\n 65.44308825471086,\n 66.94142900198796,\n 66.33531057489242,\n 66.97007154625013,\n 67.88134129373145,\n 65.92102089546943,\n 65.7896781452792,\n 67.77680599320166\n ],\n [\n 68.78026730954913,\n 68.46495903270079,\n 68.29118217530531,\n 67.90561937677037,\n 68.05316512901861,\n 70.26033810424696,\n 68.05691975265877,\n 68.76874724839202,\n 70.02772232081647,\n 70.38628221361253\n ],\n [\n \"up\",\n \"down fast\",\n \"down\",\n \"down\",\n \"down\",\n \"up\",\n \"up\",\n \"up\",\n \"down\",\n \"down\"\n ],\n [\n 1,\n 2,\n 2,\n 2,\n 2,\n 1,\n 1,\n 1,\n 2,\n 2\n ]\n ]\n }\n }\n]",
|
||||
@@ -1208,7 +1220,7 @@
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"id": "custom.hideFrom.viz",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
@@ -1271,15 +1283,9 @@
|
||||
"maxDataPoints": 10,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": ["sum"],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"Time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Min\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Max\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Info\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"State\",\n \"type\": \"enum\",\n \"typeInfo\": {\n \"frame\": \"enum\"\n },\n \"config\": {\n \"type\": {\n \"enum\": {\n \"text\": [\n \"Unknown\",\n \"Up\",\n \"Down\"\n ]\n }\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1754579008785,\n 1754580808785,\n 1754582608785,\n 1754584408785,\n 1754586208785,\n 1754588008785,\n 1754589808785,\n 1754591608785,\n 1754593408785,\n 1754595208785\n ],\n [\n 89.82213040422468,\n 89.53763263564208,\n 90.03103064454615,\n 90.51535567776907,\n 90.88960652895187,\n 91.30921636258488,\n 91.12427953645643,\n 91.1507153149385,\n 90.81021008780039,\n 91.11327042147582\n ],\n [\n 87.33191559754641,\n 88.46410173314496,\n 88.06920794235972,\n 88.98473401700574,\n 90.60349141857057,\n 90.5858441719162,\n 90.42154517148411,\n 90.32413455176213,\n 90.72055904641739,\n 88.74774424339796\n ],\n [\n 90.58845140604446,\n 90.08173133413374,\n 90.58230197880795,\n 92.55531809442493,\n 93.09165646181742,\n 93.38354884838586,\n 92.68544239576967,\n 92.15770413350477,\n 92.77335012158814,\n 92.52115072193222\n ],\n [\n \"down\",\n \"down\",\n \"up fast\",\n \"up fast\",\n \"up\",\n \"up fast\",\n \"down\",\n \"up\",\n \"down\",\n \"up\"\n ],\n [\n 2,\n 2,\n 1,\n 1,\n 1,\n 1,\n 2,\n 1,\n 2,\n 1\n ]\n ]\n }\n }\n]",
|
||||
@@ -1355,7 +1361,7 @@
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"id": "custom.hideFrom.viz",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
@@ -1418,15 +1424,9 @@
|
||||
"maxDataPoints": 10,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": ["sum"],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"Time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Min\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Max\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Info\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"State\",\n \"type\": \"enum\",\n \"typeInfo\": {\n \"frame\": \"enum\"\n },\n \"config\": {\n \"type\": {\n \"enum\": {\n \"text\": [\n \"Unknown\",\n \"Up\",\n \"Down\"\n ]\n }\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1754579078813,\n 1754580878813,\n 1754582678813,\n 1754584478813,\n 1754586278813,\n 1754588078813,\n 1754589878813,\n 1754591678813,\n 1754593478813,\n 1754595278813\n ],\n [\n 77.30289281564126,\n 87.14357945690202,\n 76.67466266903213,\n 76.27969352662608,\n 95.80292639037513,\n 75.68396049965999,\n 75.76117912054544,\n 76.05073308839697,\n 75.93744737071192,\n 75.82930696129104\n ],\n [\n 76.58900057075734,\n 76.01493865596112,\n 75.25501274661029,\n 75.52871130918913,\n 73.53324017648568,\n 74.74670600818675,\n 73.4627470190636,\n 75.38455781635166,\n 74.04318166296315,\n 74.79620005730261\n ],\n [\n 77.58417866514938,\n 77.9390885538468,\n 78.37258492198521,\n 76.82242506518101,\n 75.81423643776408,\n 75.86349731305505,\n 76.79031910137064,\n 77.71934637974913,\n 76.11981207380533,\n 77.29746416618009\n ],\n [\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"up\",\n \"up\",\n \"down\",\n \"down\"\n ],\n [\n 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 1,\n 1,\n 2,\n 2\n ]\n ]\n }\n }\n]",
|
||||
@@ -1489,7 +1489,7 @@
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"id": "custom.hideFrom.viz",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
@@ -1559,12 +1559,6 @@
|
||||
"maxDataPoints": 10,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": ["sum"],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true,
|
||||
"sortBy": [
|
||||
{
|
||||
@@ -1573,7 +1567,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"Time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Min\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Max\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Info\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"State\",\n \"type\": \"enum\",\n \"typeInfo\": {\n \"frame\": \"enum\"\n },\n \"config\": {\n \"type\": {\n \"enum\": {\n \"text\": [\n \"Unknown\",\n \"Up\",\n \"Down\"\n ]\n }\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1754579078813,\n 1754580878813,\n 1754582678813,\n 1754584478813,\n 1754586278813,\n 1754588078813,\n 1754589878813,\n 1754591678813,\n 1754593478813,\n 1754595278813\n ],\n [\n 77.30289281564126,\n 87.14357945690202,\n 76.67466266903213,\n 76.27969352662608,\n 95.80292639037513,\n 75.68396049965999,\n 75.76117912054544,\n 76.05073308839697,\n 75.93744737071192,\n 75.82930696129104\n ],\n [\n 76.58900057075734,\n 76.01493865596112,\n 75.25501274661029,\n 75.52871130918913,\n 73.53324017648568,\n 74.74670600818675,\n 73.4627470190636,\n 75.38455781635166,\n 74.04318166296315,\n 74.79620005730261\n ],\n [\n 77.58417866514938,\n 77.9390885538468,\n 78.37258492198521,\n 76.82242506518101,\n 75.81423643776408,\n 75.86349731305505,\n 76.79031910137064,\n 77.71934637974913,\n 76.11981207380533,\n 77.29746416618009\n ],\n [\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"up\",\n \"up\",\n \"down\",\n \"down\"\n ],\n [\n 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 1,\n 1,\n 2,\n 2\n ]\n ]\n }\n }\n]",
|
||||
@@ -1642,7 +1636,7 @@
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"id": "custom.hideFrom.viz",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
@@ -1746,16 +1740,10 @@
|
||||
"maxDataPoints": 10,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": ["sum"],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"name\": \"A\",\n \"fields\": [\n {\n \"name\": \"Time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time.Time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Min\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Max\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"Info\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"State\",\n \"type\": \"enum\",\n \"typeInfo\": {\n \"frame\": \"enum\"\n },\n \"config\": {\n \"type\": {\n \"enum\": {\n \"text\": [\n \"Unknown\",\n \"Up\",\n \"Down\"\n ]\n }\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1754579078813,\n 1754580878813,\n 1754582678813,\n 1754584478813,\n 1754586278813,\n 1754588078813,\n 1754589878813,\n 1754591678813,\n 1754593478813,\n 1754595278813\n ],\n [\n 77.30289281564126,\n 87.14357945690202,\n 76.67466266903213,\n 76.27969352662608,\n 95.80292639037513,\n 75.68396049965999,\n 75.76117912054544,\n 76.05073308839697,\n 75.93744737071192,\n 75.82930696129104\n ],\n [\n 76.58900057075734,\n 76.01493865596112,\n 75.25501274661029,\n 75.52871130918913,\n 73.53324017648568,\n 74.74670600818675,\n 73.4627470190636,\n 75.38455781635166,\n 74.04318166296315,\n 74.79620005730261\n ],\n [\n 77.58417866514938,\n 77.9390885538468,\n 78.37258492198521,\n 76.82242506518101,\n 75.81423643776408,\n 75.86349731305505,\n 76.79031910137064,\n 77.71934637974913,\n 76.11981207380533,\n 77.29746416618009\n ],\n [\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"down fast\",\n \"down\",\n \"up\",\n \"up\",\n \"down\",\n \"down\"\n ],\n [\n 2,\n 2,\n 2,\n 2,\n 2,\n 2,\n 1,\n 1,\n 2,\n 2\n ]\n ]\n }\n }\n]",
|
||||
@@ -1808,15 +1796,9 @@
|
||||
"id": 3,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": ["sum"],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"pluginVersion": "12.2.0",
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "a,b,c",
|
||||
@@ -1852,5 +1834,5 @@
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - Table - Kitchen Sink",
|
||||
"uid": "dcb9f5e9-8066-4397-889e-864b99555dbb",
|
||||
"version": 33
|
||||
"version": 34
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,13 +16,14 @@ weight: 1100
|
||||
|
||||
# Calculation types
|
||||
|
||||
The following table contains a list of calculations you can perform in Grafana. You can find these calculations in the **Transform** tab and in the bar gauge, gauge, and stat visualizations.
|
||||
The following table contains a list of calculations you can perform in Grafana. You can find these calculations in the **Transform** tab and in the bar gauge, gauge, stat, and table visualizations.
|
||||
|
||||
| Calculation | Description |
|
||||
| :----------------- | :-------------------------------------------------------- |
|
||||
| 1st % - 99th % | 1st - 99th percentile value. |
|
||||
| All nulls | True when all values are null |
|
||||
| All values | Array with all values |
|
||||
| All unique values | Array with all unique values |
|
||||
| All values | Array with all values |
|
||||
| All zeros | True when all values are 0 |
|
||||
| Change count | Number of times the field's value changes |
|
||||
| Count | Number of values in a field |
|
||||
@@ -35,11 +36,12 @@ The following table contains a list of calculations you can perform in Grafana.
|
||||
| Last | Last value in a field |
|
||||
| Last\* (not null) | Last, not null value in a field (also excludes NaNs) |
|
||||
| Max | Maximum value of a field |
|
||||
| Median | Median value of all values in a field |
|
||||
| Mean | Mean value of all values in a field |
|
||||
| Variance | Variance of all values in a field |
|
||||
| StdDev | Standard deviation of all values in a field |
|
||||
| Min | Minimum value of a field |
|
||||
| Min (above zero) | Minimum, positive value of a field |
|
||||
| Range | Difference between maximum and minimum values of a field |
|
||||
| StdDev | Standard deviation of all values in a field |
|
||||
| Step | Minimal interval between values of a field |
|
||||
| Total | Sum of all values in a field |
|
||||
| Variance | Variance of all values in a field |
|
||||
|
||||
@@ -216,14 +216,35 @@ This option is only available when you're editing the panel.
|
||||
|
||||
### Table footer options
|
||||
|
||||
Toggle the **Show table footer** switch on and off to control the display of the footer.
|
||||
When the toggle is switched on, you can use the table footer to show [calculations](ref:calculations) on fields.
|
||||
The table footer displays the results of calculations (and reducer functions) on fields.
|
||||
The footer is only displayed after you select an option in the **Calculation** drop-down list:
|
||||
|
||||
After you activate the table footer, make selections for the following options:
|
||||
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-table-footer-selector-v12.2.png" max-width="300px" alt="" >}}
|
||||
|
||||
- **Calculation** - The calculation that you want to apply.
|
||||
- **Count rows** - This option is displayed if you select the **Count** calculation. If you want to show the number of rows in the dataset instead of the number of values in the selected fields, toggle on the **Count rows** switch.
|
||||
- **Fields** - The fields to which you want to apply the calculation. Grafana applies the calculation to all numeric fields if you don't select a field.
|
||||
There are several calculations you can choose from including minimum, maximum, first, last, and total.
|
||||
For the full list of options, refer to [Calculations](ref:calculations).
|
||||
|
||||
In the table footer:
|
||||
|
||||
- You can apply multiple calculations at once.
|
||||
- All calculations and reducer functions are labeled except **Total** when it's the only function applied.
|
||||
- The calculations and reducer functions apply to all fields in the table, by default. To control which fields have a calculation or function applied, add the table footer in an override instead.
|
||||
- If you enable a mathematical function for a non-numeric field, nothing for that function is displayed for that field.
|
||||
|
||||
In the following image, multiple calculations—**Mean**, **Max**, and **Last**—have been applied:
|
||||
|
||||
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-table-footer-1-v12.2.png" max-width="750px" alt="" >}}
|
||||
|
||||
You can also see in the previous image that the mathematical functions, **Mean** and **Max**, haven't been applied to the text field in the table.
|
||||
Only the **Last** function has been applied to that field.
|
||||
|
||||
In the following image, the **Total** calculation has been applied, and no label is displayed because it's the only function:
|
||||
|
||||
{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-table-footer-2-v12.2.png" max-width="750px" alt="" >}}
|
||||
|
||||
{{< admonition type="note">}}
|
||||
Calculations applied to cell types like **Markdown + HTML** might have unexpected results.
|
||||
{{< /admonition>}}
|
||||
|
||||
### Cell options
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
import { getColumnIdx } from './table-utils';
|
||||
|
||||
const DASHBOARD_UID = '1ea31838-e4e8-4aa0-9333-1d4c3fa95641';
|
||||
|
||||
const waitForTableLoad = async (loc: Page | Locator) => {
|
||||
await expect(loc.locator('.rdg')).toBeVisible();
|
||||
};
|
||||
|
||||
test.describe('Panels test: Table - Footer', { tag: ['@panels', '@table'] }, () => {
|
||||
test('Footer unaffected by filtering', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '4' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Global footer, last not null'))
|
||||
).toBeVisible();
|
||||
|
||||
await waitForTableLoad(page);
|
||||
|
||||
const minColumnIdx = await getColumnIdx(page, 'Min');
|
||||
|
||||
// this is the footer cell for the "Min" column.
|
||||
await expect(
|
||||
dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.ReducerLabel)
|
||||
.nth(minColumnIdx)
|
||||
).toHaveText('Last *');
|
||||
|
||||
const minReducerValue = await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.Value)
|
||||
.nth(minColumnIdx)
|
||||
.innerText();
|
||||
|
||||
const minColumnHeader = page.getByRole('columnheader').nth(minColumnIdx);
|
||||
|
||||
// get the first value in the "State" column, filter it out, then check that it went away.
|
||||
await minColumnHeader.getByTestId(selectors.components.Panels.Visualization.TableNG.Filters.HeaderButton).click();
|
||||
const filterContainer = dashboardPage.getByGrafanaSelector(
|
||||
selectors.components.Panels.Visualization.TableNG.Filters.Container
|
||||
);
|
||||
|
||||
await expect(filterContainer).toBeVisible();
|
||||
|
||||
// select all, then click the first value to unselect it, filtering it out.
|
||||
await filterContainer.getByTestId(selectors.components.Panels.Visualization.TableNG.Filters.SelectAll).click();
|
||||
await filterContainer.getByTitle('Filter values').fill(minReducerValue);
|
||||
await filterContainer.getByTitle(minReducerValue, { exact: true }).locator('label').click();
|
||||
await filterContainer.getByRole('button', { name: 'Ok' }).click();
|
||||
|
||||
await expect(
|
||||
dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.Value)
|
||||
.nth(minColumnIdx)
|
||||
).toHaveText(minReducerValue);
|
||||
});
|
||||
|
||||
test('Footer unaffected by sorting', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '4' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Global footer, last not null'))
|
||||
).toBeVisible();
|
||||
|
||||
await waitForTableLoad(page);
|
||||
|
||||
const minColumnIdx = await getColumnIdx(page, 'Min');
|
||||
|
||||
// this is the footer cell for the "Min" column.
|
||||
await expect(
|
||||
dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.ReducerLabel)
|
||||
.nth(minColumnIdx)
|
||||
).toHaveText('Last *');
|
||||
|
||||
const minReducerValue = await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.Value)
|
||||
.nth(minColumnIdx)
|
||||
.innerText();
|
||||
|
||||
const minColumnHeader = page.getByRole('columnheader').nth(minColumnIdx);
|
||||
|
||||
// get the first value in the "State" column, filter it out, then check that it went away.
|
||||
await minColumnHeader.getByText('Min').click();
|
||||
await expect(minColumnHeader).toHaveAttribute('aria-sort', 'ascending');
|
||||
await expect(
|
||||
dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.Value)
|
||||
.nth(minColumnIdx)
|
||||
).toHaveText(minReducerValue);
|
||||
|
||||
await minColumnHeader.getByText('Min').click();
|
||||
await expect(minColumnHeader).toHaveAttribute('aria-sort', 'descending');
|
||||
await expect(
|
||||
dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.Value)
|
||||
.nth(minColumnIdx)
|
||||
).toHaveText(minReducerValue);
|
||||
|
||||
await minColumnHeader.getByText('Min').click();
|
||||
await expect(minColumnHeader).not.toHaveAttribute('aria-sort');
|
||||
await expect(
|
||||
dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.Value)
|
||||
.nth(minColumnIdx)
|
||||
).toHaveText(minReducerValue);
|
||||
});
|
||||
|
||||
test('Single-sum reducer label is hidden', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '6' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Single sum reducer'))
|
||||
).toBeVisible();
|
||||
|
||||
await waitForTableLoad(page);
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.ReducerLabel)
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.Value)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Count rows for normal case', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '7' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Count rows'))
|
||||
).toBeVisible();
|
||||
|
||||
await waitForTableLoad(page);
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.ReducerLabel)
|
||||
).toContainText('Count');
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.Value)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Count rows with a few hidden columns', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: DASHBOARD_UID,
|
||||
queryParams: new URLSearchParams({ editPanel: '8' }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Count rows, hide initial columns'))
|
||||
).toBeVisible();
|
||||
|
||||
await waitForTableLoad(page);
|
||||
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.ReducerLabel)
|
||||
).toContainText('Count');
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.TableNG.Footer.Value)
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { Page, Locator } from '@playwright/test';
|
||||
|
||||
import { test, expect, E2ESelectorGroups } from '@grafana/plugin-e2e';
|
||||
|
||||
import { getCell, getCellHeight } from './table-utils';
|
||||
import { getCell, getCellHeight, getColumnIdx } from './table-utils';
|
||||
|
||||
const DASHBOARD_UID = 'dcb9f5e9-8066-4397-889e-864b99555dbb';
|
||||
|
||||
@@ -13,24 +13,6 @@ const waitForTableLoad = async (loc: Page | Locator) => {
|
||||
await expect(loc.locator('.rdg')).toBeVisible();
|
||||
};
|
||||
|
||||
const getColumnIdx = async (loc: Page | Locator, columnName: string) => {
|
||||
// find the index of the column "Long text." The kitchen sink table will change over time, but
|
||||
// we can just find the column programatically and use it throughout the test.
|
||||
let result = -1;
|
||||
const colCount = await loc.getByRole('columnheader').count();
|
||||
for (let colIdx = 0; colIdx < colCount; colIdx++) {
|
||||
const cell = await getCell(loc, 0, colIdx);
|
||||
if ((await cell.textContent()) === columnName) {
|
||||
result = colIdx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result === -1) {
|
||||
throw new Error(`Could not find the "${columnName}" column in the table`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const disableAllTextWrap = async (loc: Page | Locator, selectors: E2ESelectorGroups) => {
|
||||
// disable text wrapping for all of the columns, since long text with links in them can push the links off the screen.
|
||||
const wrapTextToggle = loc.getByLabel('Wrap text');
|
||||
|
||||
@@ -11,3 +11,21 @@ export const getCellHeight = async (loc: Page | Locator, rowIdx: number, colIdx:
|
||||
const cell = await getCell(loc, rowIdx, colIdx);
|
||||
return (await cell.boundingBox())?.height ?? 0;
|
||||
};
|
||||
|
||||
export const getColumnIdx = async (loc: Page | Locator, columnName: string) => {
|
||||
// find the index of the column "Long text." The kitchen sink table will change over time, but
|
||||
// we can just find the column programatically and use it throughout the test.
|
||||
let result = -1;
|
||||
const colCount = await loc.getByRole('columnheader').count();
|
||||
for (let colIdx = 0; colIdx < colCount; colIdx++) {
|
||||
const cell = await getCell(loc, 0, colIdx);
|
||||
if ((await cell.textContent()) === columnName) {
|
||||
result = colIdx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result === -1) {
|
||||
throw new Error(`Could not find the "${columnName}" column in the table`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export enum ReducerID {
|
||||
median = 'median',
|
||||
first = 'first',
|
||||
count = 'count',
|
||||
countAll = 'countAll',
|
||||
range = 'range',
|
||||
diff = 'diff',
|
||||
diffperc = 'diffperc',
|
||||
@@ -132,7 +133,10 @@ export enum ReducerID {
|
||||
}
|
||||
|
||||
export function getFieldTypeForReducer(id: ReducerID, fallback: FieldType): FieldType {
|
||||
return id === ReducerID.count || id === ReducerID.distinctCount || id === ReducerID.changeCount
|
||||
return id === ReducerID.count ||
|
||||
id === ReducerID.distinctCount ||
|
||||
id === ReducerID.changeCount ||
|
||||
id === ReducerID.countAll
|
||||
? FieldType.number
|
||||
: id === ReducerID.allIsNull || id === ReducerID.allIsZero
|
||||
? FieldType.boolean
|
||||
@@ -329,6 +333,15 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
|
||||
standard: true,
|
||||
preservesUnits: false,
|
||||
},
|
||||
{
|
||||
id: ReducerID.countAll,
|
||||
name: 'Count all',
|
||||
description: 'Number of values (including empty)',
|
||||
emptyInputResult: 0,
|
||||
standard: false,
|
||||
reduce: (field: Field): FieldCalcs => ({ countAll: field.values.length }),
|
||||
preservesUnits: false,
|
||||
},
|
||||
{
|
||||
id: ReducerID.range,
|
||||
name: 'Range',
|
||||
|
||||
@@ -501,6 +501,14 @@ export const versionedComponents = {
|
||||
'12.2.0': 'data-testid tableng tooltip caret',
|
||||
},
|
||||
},
|
||||
Footer: {
|
||||
ReducerLabel: {
|
||||
'12.2.0': 'data-testid tableng footer reducer-label',
|
||||
},
|
||||
Value: {
|
||||
'12.2.0': 'data-testid tableng footer value',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -759,22 +759,6 @@ export interface TableSortByFieldState {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Footer options
|
||||
*/
|
||||
export interface TableFooterOptions {
|
||||
countRows?: boolean;
|
||||
enablePagination?: boolean;
|
||||
fields?: Array<string>;
|
||||
reducer: Array<string>; // actually 1 value
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export const defaultTableFooterOptions: Partial<TableFooterOptions> = {
|
||||
fields: [],
|
||||
reducer: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Auto mode table cell options
|
||||
*/
|
||||
@@ -992,6 +976,17 @@ export enum TableCellTooltipPlacement {
|
||||
Top = 'top',
|
||||
}
|
||||
|
||||
export interface TableFooterOptions {
|
||||
/**
|
||||
* footer reducers to apply to this field
|
||||
*/
|
||||
reducers?: Array<string>;
|
||||
}
|
||||
|
||||
export const defaultTableFooterOptions: Partial<TableFooterOptions> = {
|
||||
reducers: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
|
||||
* Generally defines alignment, filtering capabilties, display options, etc.
|
||||
@@ -1004,6 +999,10 @@ export interface TableFieldOptions extends HideableFieldConfig {
|
||||
*/
|
||||
displayMode?: TableCellDisplayMode;
|
||||
filterable?: boolean;
|
||||
/**
|
||||
* options for the footer for this field
|
||||
*/
|
||||
footer?: TableFooterOptions;
|
||||
/**
|
||||
* Hides any header for a column, useful for columns that show some static content or buttons.
|
||||
*/
|
||||
|
||||
@@ -19,15 +19,6 @@ TableSortByFieldState: {
|
||||
desc?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// Footer options
|
||||
TableFooterOptions: {
|
||||
show: bool
|
||||
reducer: [...string] // actually 1 value
|
||||
fields?: [...string]
|
||||
enablePagination?: bool
|
||||
countRows?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// Auto mode table cell options
|
||||
TableAutoCellOptions: {
|
||||
type: TableCellDisplayMode & "auto"
|
||||
@@ -101,8 +92,6 @@ TableGeoCellOptions: {
|
||||
// Height of a table cell
|
||||
TableCellHeight: "sm" | "md" | "lg" | "auto" @cuetsy(kind="enum")
|
||||
|
||||
|
||||
|
||||
// Table cell options. Each cell has a display mode
|
||||
// and other potential options for that display.
|
||||
TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions | TableMarkdownCellOptions | TableGeoCellOptions @cuetsy(kind="type")
|
||||
@@ -116,6 +105,11 @@ TableCellTooltipOptions: {
|
||||
placement?: TableCellTooltipPlacement
|
||||
}
|
||||
|
||||
TableFooterOptions: {
|
||||
// footer reducers to apply to this field
|
||||
reducers?: [...string]
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// Field options for each field within a table (e.g 10, "The String", 64.20, etc.)
|
||||
// Generally defines alignment, filtering capabilties, display options, etc.
|
||||
TableFieldOptions: {
|
||||
@@ -135,4 +129,6 @@ TableFieldOptions: {
|
||||
wrapHeaderText?: bool
|
||||
// Selecting or hovering this field will show a tooltip containing the content within the target field
|
||||
tooltip?: TableCellTooltipOptions
|
||||
// options for the footer for this field
|
||||
footer?: TableFooterOptions
|
||||
} & HideableFieldConfig @cuetsy(kind="interface")
|
||||
|
||||
+2
-16
@@ -18,9 +18,9 @@ export interface Options {
|
||||
*/
|
||||
cellHeight?: ui.TableCellHeight;
|
||||
/**
|
||||
* Controls footer options
|
||||
* Enable pagination on the table
|
||||
*/
|
||||
footer?: ui.TableFooterOptions;
|
||||
enablePagination?: boolean;
|
||||
/**
|
||||
* Represents the index of the selected frame
|
||||
*/
|
||||
@@ -51,20 +51,6 @@ export interface Options {
|
||||
|
||||
export const defaultOptions: Partial<Options> = {
|
||||
cellHeight: ui.TableCellHeight.Sm,
|
||||
footer: {
|
||||
/**
|
||||
* Controls whether the footer should be shown
|
||||
*/
|
||||
show: false,
|
||||
/**
|
||||
* Controls whether the footer should show the total number of rows on Count calculation
|
||||
*/
|
||||
countRows: false,
|
||||
/**
|
||||
* Represents the selected calculations
|
||||
*/
|
||||
reducer: [],
|
||||
},
|
||||
frameIndex: 0,
|
||||
showHeader: true,
|
||||
showTypeIcons: false,
|
||||
|
||||
@@ -102,6 +102,7 @@ export const FilterPopup = ({
|
||||
[setFilter, onClose] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
|
||||
const filterInputPlaceholder = t('grafana-ui.table.filter-popup-input-placeholder', 'Filter values');
|
||||
const clearFilterVisible = useMemo(() => filterValue !== undefined, [filterValue]);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@@ -130,7 +131,8 @@ export const FilterPopup = ({
|
||||
|
||||
<Stack gap={1}>
|
||||
<FilterInput
|
||||
placeholder={t('grafana-ui.table.filter-popup-input-placeholder', 'Filter values')}
|
||||
placeholder={filterInputPlaceholder}
|
||||
title={filterInputPlaceholder}
|
||||
onChange={setSearchFilter}
|
||||
value={searchFilter}
|
||||
/>
|
||||
|
||||
@@ -462,18 +462,22 @@ describe('TableNG', () => {
|
||||
});
|
||||
|
||||
it('renders footer with aggregations when footerOptions are provided', () => {
|
||||
const baseFrame = createBasicDataFrame();
|
||||
// Create a filter function that only shows rows with A1
|
||||
const frameWithReducers = {
|
||||
...baseFrame,
|
||||
fields: baseFrame.fields.map((field) => ({
|
||||
...field,
|
||||
config: {
|
||||
...field.config,
|
||||
custom: {
|
||||
footer: { reducers: ['sum'] },
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
const { container } = render(
|
||||
<TableNG
|
||||
enableVirtualization={false}
|
||||
data={createBasicDataFrame()}
|
||||
width={800}
|
||||
height={600}
|
||||
footerOptions={{
|
||||
show: true,
|
||||
reducer: ['sum'],
|
||||
countRows: false,
|
||||
}}
|
||||
/>
|
||||
<TableNG enableVirtualization={false} data={frameWithReducers} width={800} height={600} />
|
||||
);
|
||||
|
||||
// Check for footer row
|
||||
@@ -483,36 +487,6 @@ describe('TableNG', () => {
|
||||
// Sum of Column B values (1+2+3=6)
|
||||
expect(screen.getByText('6')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders row count in footer when countRows is true', () => {
|
||||
const { container } = render(
|
||||
<TableNG
|
||||
enableVirtualization={false}
|
||||
data={createBasicDataFrame()}
|
||||
width={800}
|
||||
height={600}
|
||||
footerOptions={{
|
||||
show: true,
|
||||
reducer: ['count'],
|
||||
countRows: true, // Enable row counting
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Check for footer row
|
||||
const footerRow = container.querySelector('.rdg-summary-row');
|
||||
expect(footerRow).toBeInTheDocument();
|
||||
|
||||
// Get the text content of the footer cells
|
||||
const footerCells = footerRow?.querySelectorAll('[role="gridcell"]');
|
||||
const footerTexts = Array.from(footerCells || []).map((cell) => cell.textContent);
|
||||
|
||||
// The first cell should contain the row count (3 rows)
|
||||
expect(footerTexts[0]).toBe('Count3');
|
||||
|
||||
// There should be no other footer cells
|
||||
expect(footerTexts[1]).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination', () => {
|
||||
@@ -1147,28 +1121,36 @@ describe('TableNG', () => {
|
||||
it('updates footer calculations when rows are filtered', () => {
|
||||
// Create a filtered frame with only the first row
|
||||
const baseFrame = createBasicDataFrame();
|
||||
|
||||
const baseFrameWithReducers = {
|
||||
...baseFrame,
|
||||
fields: baseFrame.fields.map((field) => ({
|
||||
...field,
|
||||
state: {
|
||||
calcs: {
|
||||
sum: {
|
||||
value: 6,
|
||||
formattedValue: '6',
|
||||
reducerName: 'sum',
|
||||
},
|
||||
},
|
||||
},
|
||||
config: { ...field.config, custom: { footer: { reducers: ['sum'] } } },
|
||||
})),
|
||||
};
|
||||
|
||||
const filteredFrame = {
|
||||
...baseFrame,
|
||||
length: 1,
|
||||
fields: baseFrame.fields.map((field) => ({
|
||||
...field,
|
||||
values: field.name === 'Column A' ? ['A1'] : field.name === 'Column B' ? [1] : field.values.slice(0, 1),
|
||||
config: { ...field.config, custom: { footer: { reducers: ['sum'] } } },
|
||||
})),
|
||||
};
|
||||
|
||||
// Render with unfiltered data and footer options
|
||||
const { container, rerender } = render(
|
||||
<TableNG
|
||||
enableVirtualization={false}
|
||||
data={baseFrame}
|
||||
width={800}
|
||||
height={600}
|
||||
footerOptions={{
|
||||
show: true,
|
||||
reducer: ['sum'],
|
||||
countRows: false,
|
||||
}}
|
||||
/>
|
||||
<TableNG enableVirtualization={false} data={baseFrameWithReducers} width={800} height={600} />
|
||||
);
|
||||
|
||||
// Check initial footer sum (1+2+3=6)
|
||||
@@ -1183,20 +1165,7 @@ describe('TableNG', () => {
|
||||
expect(initialFooterTexts[1]).toBe('6');
|
||||
|
||||
// Rerender with filtered data
|
||||
rerender(
|
||||
<TableNG
|
||||
enableVirtualization={false}
|
||||
data={filteredFrame}
|
||||
width={800}
|
||||
height={600}
|
||||
footerOptions={{
|
||||
show: true,
|
||||
reducer: ['sum'],
|
||||
countRows: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
rerender(<TableNG enableVirtualization={false} data={filteredFrame} width={800} height={600} />);
|
||||
// Check filtered footer sum (should be 1)
|
||||
const filteredFooter = container.querySelector('.rdg-summary-row');
|
||||
expect(filteredFooter).toBeInTheDocument();
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
Field,
|
||||
FieldType,
|
||||
getDisplayProcessor,
|
||||
ReducerID,
|
||||
} from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { FieldColorModeId, TableCellTooltipPlacement } from '@grafana/schema';
|
||||
@@ -40,6 +39,7 @@ import { DataLinksActionsTooltipState } from '../utils';
|
||||
import { getCellRenderer, getCellSpecificStyles } from './Cells/renderers';
|
||||
import { HeaderCell } from './components/HeaderCell';
|
||||
import { RowExpander } from './components/RowExpander';
|
||||
import { SummaryCell } from './components/SummaryCell';
|
||||
import { TableCellActions } from './components/TableCellActions';
|
||||
import { TableCellTooltip } from './components/TableCellTooltip';
|
||||
import { COLUMN, TABLE } from './constants';
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
useColumnResize,
|
||||
useColWidths,
|
||||
useFilteredRows,
|
||||
useFooterCalcs,
|
||||
useHeaderHeight,
|
||||
usePaginatedRows,
|
||||
useRowHeight,
|
||||
@@ -57,7 +56,6 @@ import {
|
||||
import {
|
||||
getCellActionStyles,
|
||||
getDefaultCellStyles,
|
||||
getFooterStyles,
|
||||
getGridStyles,
|
||||
getHeaderCellStyles,
|
||||
getLinkStyles,
|
||||
@@ -77,6 +75,7 @@ import {
|
||||
import {
|
||||
applySort,
|
||||
canFieldBeColorized,
|
||||
calculateFooterHeight,
|
||||
createTypographyContext,
|
||||
displayJsonValue,
|
||||
extractPixelValue,
|
||||
@@ -108,7 +107,7 @@ export function TableNG(props: TableNGProps) {
|
||||
enablePagination = false,
|
||||
enableSharedCrosshair = false,
|
||||
enableVirtualization,
|
||||
footerOptions,
|
||||
fieldConfig,
|
||||
frozenColumns = 0,
|
||||
getActions = () => [],
|
||||
height,
|
||||
@@ -125,6 +124,12 @@ export function TableNG(props: TableNGProps) {
|
||||
width,
|
||||
} = props;
|
||||
|
||||
const hasFooter = useMemo(
|
||||
() => data.fields.some((field) => field.config?.custom?.footer?.reducers?.length ?? false),
|
||||
[data.fields]
|
||||
);
|
||||
const footerHeight = hasFooter ? calculateFooterHeight(data, fieldConfig) : 0;
|
||||
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getGridStyles, enablePagination, transparent);
|
||||
const panelContext = usePanelContext();
|
||||
@@ -135,13 +140,6 @@ export function TableNG(props: TableNGProps) {
|
||||
);
|
||||
|
||||
const hasHeader = !noHeader;
|
||||
const hasFooter = Boolean(footerOptions?.show && footerOptions.reducer?.length);
|
||||
const isCountRowsSet = Boolean(
|
||||
footerOptions?.countRows &&
|
||||
footerOptions.reducer &&
|
||||
footerOptions.reducer.length &&
|
||||
footerOptions.reducer[0] === ReducerID.count
|
||||
);
|
||||
|
||||
const resizeHandler = useColumnResize(onColumnResize);
|
||||
|
||||
@@ -228,18 +226,11 @@ export function TableNG(props: TableNGProps) {
|
||||
enabled: enablePagination,
|
||||
width: availableWidth,
|
||||
height,
|
||||
headerHeight,
|
||||
footerHeight: hasFooter ? (typeof defaultRowHeight === 'number' ? defaultRowHeight : TABLE.MAX_CELL_HEIGHT) : 0,
|
||||
footerHeight,
|
||||
headerHeight: hasHeader ? TABLE.HEADER_ROW_HEIGHT : 0,
|
||||
rowHeight,
|
||||
});
|
||||
|
||||
// Create a map of column key to text wrap
|
||||
const footerCalcs = useFooterCalcs(sortedRows, visibleFields, {
|
||||
enabled: hasFooter,
|
||||
footerOptions,
|
||||
isCountRowsSet,
|
||||
});
|
||||
|
||||
// normalize the row height into a function which returns a number, so we avoid a bunch of conditionals during rendering.
|
||||
const rowHeightFn = useMemo((): ((row: TableRow) => number) => {
|
||||
if (typeof rowHeight === 'function') {
|
||||
@@ -279,8 +270,22 @@ export function TableNG(props: TableNGProps) {
|
||||
sortColumns,
|
||||
rowHeight,
|
||||
bottomSummaryRows: hasFooter ? [{}] : undefined,
|
||||
summaryRowHeight: footerHeight,
|
||||
headerRowClass: styles.headerRow,
|
||||
headerRowHeight: noHeader ? 0 : TABLE.HEADER_ROW_HEIGHT,
|
||||
}) satisfies Partial<DataGridProps<TableRow, TableSummaryRow>>,
|
||||
[enableVirtualization, resizeHandler, sortColumns, rowHeight, hasFooter, setSortColumns, onSortByChange]
|
||||
[
|
||||
enableVirtualization,
|
||||
hasFooter,
|
||||
resizeHandler,
|
||||
sortColumns,
|
||||
rowHeight,
|
||||
styles.headerRow,
|
||||
noHeader,
|
||||
setSortColumns,
|
||||
onSortByChange,
|
||||
footerHeight,
|
||||
]
|
||||
);
|
||||
|
||||
const buildNestedTableExpanderColumn = useCallback(
|
||||
@@ -404,7 +409,6 @@ export function TableNG(props: TableNGProps) {
|
||||
// contents with flexbox. We always just get both and provide both when styling the cell.
|
||||
const textAlign = getAlignment(field);
|
||||
const justifyContent = getJustifyContent(textAlign);
|
||||
const footerStyles = getFooterStyles(justifyContent);
|
||||
const displayName = getDisplayName(field);
|
||||
const headerCellClass = getHeaderCellStyles(theme, justifyContent);
|
||||
const CellType = getCellRenderer(field, cellOptions);
|
||||
@@ -637,19 +641,7 @@ export function TableNG(props: TableNGProps) {
|
||||
showTypeIcons={showTypeIcons}
|
||||
/>
|
||||
),
|
||||
renderSummaryCell: () => {
|
||||
if (isCountRowsSet && i === 0) {
|
||||
return (
|
||||
<div className={footerStyles.footerCellCountRows}>
|
||||
<span>
|
||||
<Trans i18nKey="grafana-ui.table.count">Count</Trans>
|
||||
</span>
|
||||
<span>{footerCalcs[i]}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div className={footerStyles.footerCell}>{footerCalcs[i]}</div>;
|
||||
},
|
||||
renderSummaryCell: () => <SummaryCell rows={rows} field={field} omitCountAll={i > 0} />,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -662,12 +654,10 @@ export function TableNG(props: TableNGProps) {
|
||||
data,
|
||||
disableSanitizeHtml,
|
||||
filter,
|
||||
footerCalcs,
|
||||
frozenColumns,
|
||||
getCellActions,
|
||||
getCellColorInlineStyles,
|
||||
getTextColorForBackground,
|
||||
isCountRowsSet,
|
||||
maxRowHeight,
|
||||
numFrozenColsFullyInView,
|
||||
onCellFilterAdded,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { screen, render } from '@testing-library/react';
|
||||
|
||||
import { Field, FieldType } from '@grafana/data';
|
||||
|
||||
import { SummaryCell } from './SummaryCell';
|
||||
|
||||
describe('SummaryCell', () => {
|
||||
const rows = [
|
||||
{ Field1: 1, Field2: 3, Text: 'a', __depth: 0, __index: 0 },
|
||||
{ Field1: 2, Field2: 10, Text: 'b', __depth: 0, __index: 1 },
|
||||
{ Field1: 3, Text: 'efghi', __depth: 0, __index: 2 },
|
||||
];
|
||||
|
||||
const numericField: Field = {
|
||||
name: 'Field1',
|
||||
type: FieldType.number,
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: {
|
||||
footer: {
|
||||
reducers: ['sum'],
|
||||
},
|
||||
},
|
||||
},
|
||||
display: (value: unknown) => ({
|
||||
text: String(value),
|
||||
numeric: Number(value),
|
||||
color: undefined,
|
||||
prefix: undefined,
|
||||
suffix: undefined,
|
||||
}),
|
||||
state: {},
|
||||
getLinks: undefined,
|
||||
};
|
||||
|
||||
const numericField2: Field = {
|
||||
name: 'Field2',
|
||||
type: FieldType.number,
|
||||
values: [3, 10],
|
||||
config: {
|
||||
custom: {
|
||||
footer: {
|
||||
reducers: ['sum'],
|
||||
},
|
||||
},
|
||||
},
|
||||
display: (value: unknown) => ({
|
||||
text: String(value),
|
||||
numeric: Number(value),
|
||||
color: undefined,
|
||||
prefix: undefined,
|
||||
suffix: undefined,
|
||||
}),
|
||||
state: {},
|
||||
getLinks: undefined,
|
||||
};
|
||||
|
||||
const textField: Field = {
|
||||
name: 'Text',
|
||||
type: FieldType.string,
|
||||
values: ['a', 'b', 'c'],
|
||||
config: {
|
||||
custom: {
|
||||
reducers: ['sum'],
|
||||
},
|
||||
},
|
||||
display: (value: unknown) => ({
|
||||
text: String(value),
|
||||
numeric: 0,
|
||||
color: undefined,
|
||||
prefix: undefined,
|
||||
suffix: undefined,
|
||||
}),
|
||||
state: {},
|
||||
getLinks: undefined,
|
||||
};
|
||||
|
||||
it('should calculate sum for numeric fields', () => {
|
||||
render(<SummaryCell rows={rows} field={numericField} />);
|
||||
expect(screen.getByText('6')).toBeInTheDocument(); // 1 + 2 + 3
|
||||
});
|
||||
|
||||
it('should hide the label for the sum reducer if its the only reducer', () => {
|
||||
render(<SummaryCell rows={rows} field={numericField} />);
|
||||
expect(screen.queryByText('Total')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('6')).toBeInTheDocument(); // 1 + 2 + 3
|
||||
});
|
||||
|
||||
it('should show the label for the sum reducer if its not the only reducer', () => {
|
||||
const numericFieldWithMultipleReducers = {
|
||||
...numericField,
|
||||
config: {
|
||||
...numericField.config,
|
||||
custom: { ...numericField.config.custom, footer: { reducers: ['sum', 'mean'] } },
|
||||
},
|
||||
};
|
||||
render(<SummaryCell rows={rows} field={numericFieldWithMultipleReducers} />);
|
||||
expect(screen.getByText('Total')).toBeInTheDocument();
|
||||
expect(screen.getByText('6')).toBeInTheDocument(); // 1 + 2 + 3
|
||||
expect(screen.getByText('Mean')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument(); // (1 + 2 + 3) / 3
|
||||
});
|
||||
|
||||
it('should calculate mean for numeric fields', () => {
|
||||
const newNumericField = {
|
||||
...numericField,
|
||||
config: {
|
||||
...numericField.config,
|
||||
custom: { ...numericField.config.custom, footer: { reducers: ['mean'] } },
|
||||
},
|
||||
};
|
||||
render(<SummaryCell rows={rows} field={newNumericField} />);
|
||||
expect(screen.getByText('Mean')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument(); // (1 + 2 + 3) / 3
|
||||
});
|
||||
|
||||
it('should render an empty summary cell for non-numeric fields with numeric reducers', () => {
|
||||
render(<SummaryCell rows={rows} field={textField} />);
|
||||
expect(screen.getByTestId('summary-cell-empty')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the summary cell if a non-numeric reducer is set for a non-numeric field', () => {
|
||||
const textFieldNonNumericReducer = {
|
||||
...textField,
|
||||
config: { ...textField.config, custom: { ...textField.config.custom, footer: { reducers: ['last'] } } },
|
||||
};
|
||||
render(<SummaryCell rows={rows} field={textFieldNonNumericReducer} />);
|
||||
expect(screen.getByText('Last')).toBeInTheDocument();
|
||||
expect(screen.getByText('efghi')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render an empty summary cell if no reducers are set', () => {
|
||||
const numericFieldNoReducers = {
|
||||
...numericField,
|
||||
config: { ...numericField.config, custom: { ...numericField.config.custom, footer: { reducers: [] } } },
|
||||
};
|
||||
render(<SummaryCell rows={rows} field={numericFieldNoReducers} />);
|
||||
expect(screen.getByTestId('summary-cell-empty')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should correctly calculate sum for numeric fields based on selected fields', () => {
|
||||
render(
|
||||
<>
|
||||
<SummaryCell rows={rows} field={numericField} />
|
||||
<SummaryCell rows={rows} field={numericField2} />
|
||||
</>
|
||||
);
|
||||
|
||||
expect(screen.getByText('6')).toBeInTheDocument(); // 1 + 2 + 3
|
||||
expect(screen.getByText('13')).toBeInTheDocument(); // 3 + 10
|
||||
});
|
||||
|
||||
it('should render summary cells with a mix of numeric-and non-numeric reducers and fields', () => {
|
||||
const fields = [numericField, numericField2, textField].map((field) => ({
|
||||
...field,
|
||||
config: {
|
||||
...field.config,
|
||||
custom: {
|
||||
...field.config.custom,
|
||||
footer: { reducers: ['sum', 'mean', 'last'] },
|
||||
},
|
||||
},
|
||||
}));
|
||||
render(
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<SummaryCell key={index} rows={rows} field={field} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
expect(screen.getByText('13')).toBeInTheDocument(); // sum
|
||||
expect(screen.getByText('6.5')).toBeInTheDocument(); // mean
|
||||
expect(screen.getByText('efghi')).toBeInTheDocument(); // last
|
||||
});
|
||||
|
||||
// TODO: add test for noFormattingReducers
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
GrafanaTheme2,
|
||||
Field,
|
||||
FieldState,
|
||||
FieldType,
|
||||
reduceField,
|
||||
fieldReducers,
|
||||
formattedValueToString,
|
||||
ReducerID,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { useStyles2 } from '../../../../themes/ThemeContext';
|
||||
import { TableRow } from '../types';
|
||||
import { getDisplayName } from '../utils';
|
||||
|
||||
interface SummaryCellProps {
|
||||
rows: TableRow[];
|
||||
field: Field;
|
||||
omitCountAll?: boolean;
|
||||
}
|
||||
|
||||
export interface ReducerResult {
|
||||
value: number | null;
|
||||
formattedValue: string;
|
||||
reducerName: string;
|
||||
}
|
||||
|
||||
interface FooterFieldState extends FieldState {
|
||||
lastProcessedRowCount: number;
|
||||
}
|
||||
|
||||
function isReducer(maybeReducer: string): maybeReducer is ReducerID {
|
||||
return maybeReducer in ReducerID;
|
||||
}
|
||||
|
||||
const nonMathReducers = new Set<ReducerID>([
|
||||
ReducerID.allValues,
|
||||
ReducerID.changeCount,
|
||||
ReducerID.count,
|
||||
ReducerID.countAll,
|
||||
ReducerID.distinctCount,
|
||||
ReducerID.first,
|
||||
ReducerID.firstNotNull,
|
||||
ReducerID.last,
|
||||
ReducerID.lastNotNull,
|
||||
ReducerID.uniqueValues,
|
||||
]);
|
||||
const isNonMathReducer = (reducer: string) => isReducer(reducer) && nonMathReducers.has(reducer);
|
||||
|
||||
const noFormattingReducers = new Set<ReducerID>([ReducerID.count, ReducerID.countAll]);
|
||||
const shouldReducerSkipFormatting = (reducer: string) => isReducer(reducer) && noFormattingReducers.has(reducer);
|
||||
|
||||
export const SummaryCell = ({ rows, field, omitCountAll = false }: SummaryCellProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const displayName = getDisplayName(field);
|
||||
|
||||
const reducerResultsEntries = useMemo<Array<[string, ReducerResult | null]>>(() => {
|
||||
const reducers: string[] = field.config.custom?.footer?.reducers ?? [];
|
||||
|
||||
if (
|
||||
reducers.length === 0 ||
|
||||
(field.type !== FieldType.number && reducers.every((reducerId) => !isNonMathReducer(reducerId)))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Create a new state object that matches the original behavior exactly
|
||||
const newState: FooterFieldState = {
|
||||
lastProcessedRowCount: 0,
|
||||
...(field.state || {}), // Preserve any existing state properties
|
||||
};
|
||||
|
||||
// Assign back to field
|
||||
field.state = newState;
|
||||
|
||||
const currentRowCount = rows.length;
|
||||
const lastRowCount = newState.lastProcessedRowCount;
|
||||
|
||||
// Check if we need to invalidate the cache
|
||||
if (lastRowCount !== currentRowCount) {
|
||||
// Cache should be invalidated as row count has changed
|
||||
if (newState.calcs) {
|
||||
delete newState.calcs;
|
||||
}
|
||||
// Update the row count tracker
|
||||
newState.lastProcessedRowCount = currentRowCount;
|
||||
}
|
||||
|
||||
// Calculate all specified reducers
|
||||
const results: Record<string, number | null> = reduceField({
|
||||
field: {
|
||||
...field,
|
||||
values: rows.map((row) => row[displayName]),
|
||||
},
|
||||
reducers,
|
||||
});
|
||||
|
||||
return reducers.map((reducerId) => {
|
||||
// For number fields, show all reducers
|
||||
// For non-number fields, only show special count reducers
|
||||
if (results[reducerId] === undefined || (field.type !== FieldType.number && !isNonMathReducer(reducerId))) {
|
||||
return [reducerId, null];
|
||||
}
|
||||
|
||||
const value = results[reducerId];
|
||||
const reducerName = fieldReducers.get(reducerId)?.name || reducerId;
|
||||
const formattedValue =
|
||||
field.display && !shouldReducerSkipFormatting(reducerId)
|
||||
? formattedValueToString(field.display(value))
|
||||
: String(value);
|
||||
|
||||
return [
|
||||
reducerId,
|
||||
{
|
||||
value,
|
||||
formattedValue,
|
||||
reducerName,
|
||||
},
|
||||
];
|
||||
});
|
||||
}, [field, rows, displayName]);
|
||||
|
||||
const isSingleSumReducer = useMemo(
|
||||
() => reducerResultsEntries.every(([item]) => item === 'sum'),
|
||||
[reducerResultsEntries]
|
||||
);
|
||||
|
||||
if (reducerResultsEntries.length === 0) {
|
||||
return <div data-testid="summary-cell-empty" className={styles.footerCell} />;
|
||||
}
|
||||
|
||||
// Render each reducer in the footer
|
||||
return (
|
||||
<div className={styles.footerCell}>
|
||||
{reducerResultsEntries.map(([reducerId, reducerResultEntry]) => {
|
||||
const isCountAll = reducerId === ReducerID.countAll;
|
||||
|
||||
// empty reducer entry, but there may be more after - render a spacer.
|
||||
if ((isCountAll && omitCountAll) || reducerResultEntry === null) {
|
||||
return (
|
||||
<div key={reducerId} className={styles.footerItem}>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { reducerName, formattedValue } = reducerResultEntry;
|
||||
|
||||
const canonicalReducerName = isCountAll ? t('grafana-ui.table.footer.reducer.count', 'Count') : reducerName;
|
||||
|
||||
return (
|
||||
<div key={reducerId} className={cx(styles.footerItem, isSingleSumReducer && styles.sumReducer)}>
|
||||
{!isSingleSumReducer && (
|
||||
<div
|
||||
data-testid={selectors.components.Panels.Visualization.TableNG.Footer.ReducerLabel}
|
||||
className={styles.footerItemLabel}
|
||||
>
|
||||
{canonicalReducerName}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
data-testid={selectors.components.Panels.Visualization.TableNG.Footer.Value}
|
||||
className={styles.footerItemValue}
|
||||
>
|
||||
{formattedValue}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
footerCell: css({
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}),
|
||||
footerItem: css({
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}),
|
||||
footerItemLabel: css({
|
||||
// Handle overflow reducer name collision with footer item value
|
||||
maxWidth: '75%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: theme.colors.text.secondary,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
fontWeight: theme.typography.fontWeightLight,
|
||||
marginRight: theme.spacing(1),
|
||||
textTransform: 'uppercase',
|
||||
}),
|
||||
footerItemValue: css({
|
||||
maxWidth: '75%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
}),
|
||||
sumReducer: css({
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'end',
|
||||
width: '100%',
|
||||
}),
|
||||
});
|
||||
@@ -9,11 +9,12 @@ export const COLUMN = {
|
||||
/** Table layout and display constants */
|
||||
export const TABLE = {
|
||||
CELL_PADDING: 6,
|
||||
LINE_HEIGHT: 22,
|
||||
HEADER_ROW_HEIGHT: 28,
|
||||
MAX_CELL_HEIGHT: 48,
|
||||
PAGINATION_LIMIT: 750,
|
||||
SCROLL_BAR_WIDTH: 8,
|
||||
SCROLL_BAR_MARGIN: 2,
|
||||
LINE_HEIGHT: 22,
|
||||
HEADER_HEIGHT: 28,
|
||||
NESTED_NO_DATA_HEIGHT: 60,
|
||||
BORDER_RIGHT: 1,
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
|
||||
import { cacheFieldDisplayNames, createDataFrame, Field, FieldType } from '@grafana/data';
|
||||
import { createDataFrame, Field, FieldType } from '@grafana/data';
|
||||
import { TableCellDisplayMode } from '@grafana/schema';
|
||||
|
||||
import { TABLE } from './constants';
|
||||
import {
|
||||
useFilteredRows,
|
||||
usePaginatedRows,
|
||||
useSortedRows,
|
||||
useFooterCalcs,
|
||||
useHeaderHeight,
|
||||
useRowHeight,
|
||||
} from './hooks';
|
||||
import { useFilteredRows, usePaginatedRows, useSortedRows, useHeaderHeight, useRowHeight } from './hooks';
|
||||
import { TableRow } from './types';
|
||||
import { createTypographyContext } from './utils';
|
||||
|
||||
@@ -220,210 +213,6 @@ describe('TableNG hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('useFooterCalcs', () => {
|
||||
const rows = [
|
||||
{ 'Field 1': 1, Text: 'a', __depth: 0, __index: 0 },
|
||||
{ 'Field 1': 2, Text: 'b', __depth: 0, __index: 1 },
|
||||
{ 'Field 1': 3, Text: 'c', __depth: 0, __index: 2 },
|
||||
{ 'Field 2': 3, Text: 'd', __depth: 0, __index: 3 },
|
||||
{ 'Field 2': 10, Text: 'e', __depth: 0, __index: 4 },
|
||||
];
|
||||
|
||||
const numericField: Field = {
|
||||
name: 'Field1',
|
||||
type: FieldType.number,
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: {},
|
||||
displayName: 'Field 1',
|
||||
},
|
||||
display: (value: unknown) => ({
|
||||
text: String(value),
|
||||
numeric: Number(value),
|
||||
color: undefined,
|
||||
prefix: undefined,
|
||||
suffix: undefined,
|
||||
}),
|
||||
getLinks: undefined,
|
||||
};
|
||||
|
||||
const numericField2: Field = {
|
||||
name: 'Field2',
|
||||
type: FieldType.number,
|
||||
values: [3, 10],
|
||||
config: {
|
||||
custom: {},
|
||||
displayName: 'Field 2',
|
||||
},
|
||||
display: (value: unknown) => ({
|
||||
text: String(value),
|
||||
numeric: Number(value),
|
||||
color: undefined,
|
||||
prefix: undefined,
|
||||
suffix: undefined,
|
||||
}),
|
||||
getLinks: undefined,
|
||||
};
|
||||
|
||||
const textField: Field = {
|
||||
name: 'Text',
|
||||
type: FieldType.string,
|
||||
values: ['a', 'b', 'c'],
|
||||
config: { custom: {} },
|
||||
display: (value: unknown) => ({
|
||||
text: String(value),
|
||||
numeric: 0,
|
||||
color: undefined,
|
||||
prefix: undefined,
|
||||
suffix: undefined,
|
||||
}),
|
||||
getLinks: undefined,
|
||||
};
|
||||
|
||||
it('should calculate sum for numeric fields', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [textField, numericField] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: true,
|
||||
footerOptions: { show: true, reducer: ['sum'] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(['Total', '6']); // 1 + 2 + 3
|
||||
});
|
||||
|
||||
it('should calculate mean for numeric fields', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [textField, numericField] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: true,
|
||||
footerOptions: { show: true, reducer: ['mean'] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(['Mean', '2']); // (1 + 2 + 3) / 3
|
||||
});
|
||||
|
||||
it('should return an empty string for non-numeric fields', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [textField, textField] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: true,
|
||||
footerOptions: { show: true, reducer: ['sum'] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(['Total', '']);
|
||||
});
|
||||
|
||||
it('should return empty array if no footerOptions are provided', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: true,
|
||||
footerOptions: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when footer is disabled', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: false,
|
||||
footerOptions: { show: true, reducer: ['sum'] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when reducer is undefined', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [textField, textField] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: true,
|
||||
footerOptions: { show: true, reducer: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when reducer is empty', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: true,
|
||||
footerOptions: { show: true, reducer: [] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty string if fields array doesnt include this field', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: true,
|
||||
footerOptions: { show: true, reducer: ['sum'], fields: ['Field2', 'Field3'] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(['Total', '', '13']);
|
||||
});
|
||||
|
||||
it('should return the calculation if fields array includes this field', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: true,
|
||||
footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field2', 'Field3'] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(['Total', '6', '13']);
|
||||
});
|
||||
|
||||
it('should return the calculation if fields array includes this field by either name or display name', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: true,
|
||||
footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field 2'] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(['Total', '6', '13']);
|
||||
});
|
||||
|
||||
it('should not return the reducer label in the first column if there is a calc to render', () => {
|
||||
const { result } = renderHook(() => {
|
||||
const data = createDataFrame({ fields: [numericField, numericField2] });
|
||||
cacheFieldDisplayNames([data]);
|
||||
return useFooterCalcs(rows, data.fields, {
|
||||
enabled: true,
|
||||
footerOptions: { show: true, reducer: ['sum'], fields: [] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current).toEqual(['6', '13']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useHeaderHeight', () => {
|
||||
const typographyCtx = createTypographyContext(14, 'sans-serif');
|
||||
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { useState, useMemo, useCallback, useRef, useLayoutEffect, RefObject, CSSProperties, useEffect } from 'react';
|
||||
import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid';
|
||||
|
||||
import {
|
||||
compareArrayValues,
|
||||
Field,
|
||||
fieldReducers,
|
||||
FieldType,
|
||||
formattedValueToString,
|
||||
reduceField,
|
||||
} from '@grafana/data';
|
||||
import { compareArrayValues, Field, formattedValueToString } from '@grafana/data';
|
||||
|
||||
import { TableColumnResizeActionCallback } from '../types';
|
||||
|
||||
import { TABLE } from './constants';
|
||||
import { FilterType, TableFooterCalc, TableRow, TableSortByFieldState, TableSummaryRow, TypographyCtx } from './types';
|
||||
import { FilterType, TableRow, TableSortByFieldState, TableSummaryRow, TypographyCtx } from './types';
|
||||
import {
|
||||
getDisplayName,
|
||||
processNestedTableRows,
|
||||
@@ -259,73 +252,6 @@ export function usePaginatedRows(
|
||||
};
|
||||
}
|
||||
|
||||
export interface FooterCalcsOptions {
|
||||
enabled?: boolean;
|
||||
isCountRowsSet?: boolean;
|
||||
footerOptions?: TableFooterCalc;
|
||||
}
|
||||
|
||||
export function useFooterCalcs(
|
||||
rows: TableRow[],
|
||||
// it's very important that this is the _visible_ fields.
|
||||
fields: Field[],
|
||||
{ enabled, footerOptions, isCountRowsSet }: FooterCalcsOptions
|
||||
): string[] {
|
||||
return useMemo(() => {
|
||||
const footerReducers = footerOptions?.reducer;
|
||||
|
||||
if (!enabled || !footerOptions || !Array.isArray(footerReducers) || !footerReducers.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fieldNameSet = footerOptions.fields?.length ? new Set(footerOptions.fields) : null;
|
||||
|
||||
return fields.map((field, index) => {
|
||||
if (field.state?.calcs) {
|
||||
delete field.state?.calcs;
|
||||
}
|
||||
|
||||
if (isCountRowsSet) {
|
||||
return index === 0 ? `${rows.length}` : '';
|
||||
}
|
||||
|
||||
let emptyValue = '';
|
||||
if (index === 0) {
|
||||
const footerCalcReducer = footerReducers[0];
|
||||
emptyValue = footerCalcReducer ? fieldReducers.get(footerCalcReducer).name : '';
|
||||
}
|
||||
|
||||
if (field.type !== FieldType.number) {
|
||||
return emptyValue;
|
||||
}
|
||||
|
||||
// if field.display is undefined, don't throw
|
||||
const displayFn = field.display;
|
||||
if (!displayFn) {
|
||||
return emptyValue;
|
||||
}
|
||||
|
||||
// If fields array is specified, only show footer for fields included in that array.
|
||||
// the array can include either the display name or the field name. we don't use a field matcher
|
||||
// because that requires us to drill the data frame down here.
|
||||
if (fieldNameSet && !fieldNameSet.has(getDisplayName(field)) && !fieldNameSet.has(field.name)) {
|
||||
return emptyValue;
|
||||
}
|
||||
|
||||
const calc = footerReducers[0];
|
||||
const value = reduceField({
|
||||
field: {
|
||||
...field,
|
||||
values: rows.map((row) => row[getDisplayName(field)]),
|
||||
},
|
||||
reducers: footerReducers,
|
||||
})[calc];
|
||||
|
||||
return formattedValueToString(displayFn(value));
|
||||
});
|
||||
}, [fields, enabled, footerOptions, isCountRowsSet, rows]);
|
||||
}
|
||||
|
||||
const ICON_WIDTH = 16;
|
||||
const ICON_GAP = 4;
|
||||
|
||||
|
||||
@@ -112,11 +112,6 @@ export const getGridStyles = (theme: GrafanaTheme2, enablePagination?: boolean,
|
||||
};
|
||||
};
|
||||
|
||||
export const getFooterStyles = (justifyContent: Property.JustifyContent) => ({
|
||||
footerCellCountRows: css({ display: 'flex', justifyContent: 'space-between' }),
|
||||
footerCell: css({ display: 'flex', justifyContent: justifyContent || 'space-between' }),
|
||||
});
|
||||
|
||||
export const getHeaderCellStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) =>
|
||||
css({
|
||||
display: 'flex',
|
||||
|
||||
@@ -104,14 +104,6 @@ export interface TableSortByFieldState {
|
||||
desc?: boolean;
|
||||
}
|
||||
|
||||
export interface TableFooterCalc {
|
||||
show: boolean;
|
||||
reducer?: string[];
|
||||
fields?: string[];
|
||||
enablePagination?: boolean;
|
||||
countRows?: boolean;
|
||||
}
|
||||
|
||||
export interface BaseTableProps {
|
||||
ariaLabel?: string;
|
||||
data: DataFrame;
|
||||
@@ -127,7 +119,6 @@ export interface BaseTableProps {
|
||||
onColumnResize?: TableColumnResizeActionCallback;
|
||||
onSortByChange?: TableSortByActionCallback;
|
||||
onCellFilterAdded?: TableFilterActionCallback;
|
||||
footerOptions?: TableFooterCalc;
|
||||
footerValues?: FooterItem[];
|
||||
frozenColumns?: number;
|
||||
enablePagination?: boolean;
|
||||
|
||||
@@ -857,8 +857,6 @@ describe('TableNG utils', () => {
|
||||
expect(extractPixelValue('not-a-number')).toBe(0);
|
||||
expect(extractPixelValue('px')).toBe(0);
|
||||
expect(extractPixelValue('')).toBe(0);
|
||||
expect(extractPixelValue(null as any)).toBe(0);
|
||||
expect(extractPixelValue(undefined as any)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Count, varPreLine } from 'uwrap';
|
||||
import {
|
||||
FieldType,
|
||||
Field,
|
||||
FieldConfigSource,
|
||||
formattedValueToString,
|
||||
GrafanaTheme2,
|
||||
DisplayValue,
|
||||
@@ -692,7 +693,7 @@ export const frameToRecords = (frame: DataFrame): TableRow[] => {
|
||||
|
||||
// Creates a function that converts a DataFrame into an array of TableRows
|
||||
// Uses new Function() for performance as it's faster than creating rows using loops
|
||||
const convert = new Function('frame', fnBody) as unknown as FrameToRowsConverter;
|
||||
const convert = new Function('frame', fnBody) as FrameToRowsConverter;
|
||||
return convert(frame);
|
||||
};
|
||||
|
||||
@@ -843,6 +844,58 @@ export const processNestedTableRows = (
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Get the maximum number of reducers across all fields
|
||||
*/
|
||||
const getMaxReducerCount = (dataFrame: DataFrame, fieldConfig?: FieldConfigSource): number => {
|
||||
// Filter to only numeric fields that can have reducers
|
||||
const numericFields = dataFrame.fields.filter(({ type }) => type === FieldType.number);
|
||||
|
||||
// If there are no numeric fields, return 0
|
||||
if (numericFields.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Map each field to its reducer count (direct config or override)
|
||||
const reducerCounts = numericFields.map((field) => {
|
||||
// Get the direct reducer count from the field config
|
||||
const directReducers = field.config?.custom?.footer?.reducers ?? [];
|
||||
let reducerCount = directReducers.length;
|
||||
|
||||
// Check for overrides if field config is available
|
||||
if (fieldConfig?.overrides) {
|
||||
// Find override that matches this field
|
||||
const override = fieldConfig.overrides.find(
|
||||
({ matcher: { id, options } }) => id === 'byName' && options === getDisplayName(field)
|
||||
);
|
||||
|
||||
// Check if there's a footer reducer property in the override
|
||||
const footerProperty = override?.properties?.find(({ id }) => id === 'custom.footer.reducers');
|
||||
if (footerProperty?.value && Array.isArray(footerProperty.value)) {
|
||||
// If override exists, it takes precedence over direct config
|
||||
reducerCount = footerProperty.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
return reducerCount;
|
||||
});
|
||||
|
||||
// Return the maximum count or 0 if no reducers found
|
||||
return reducerCounts.length > 0 ? Math.max(...reducerCounts) : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Calculate the footer height based on the maximum reducer count
|
||||
*/
|
||||
export const calculateFooterHeight = (dataFrame: DataFrame, fieldConfig?: FieldConfigSource) => {
|
||||
const maxReducerCount = getMaxReducerCount(dataFrame, fieldConfig);
|
||||
// Base height (+ padding) + height per reducer
|
||||
return maxReducerCount * TABLE.LINE_HEIGHT + TABLE.CELL_PADDING * 2;
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* returns the display name of a field
|
||||
* returns the display name of a field.
|
||||
* We intentionally do not want to use @grafana/data's getFieldDisplayName here,
|
||||
* instead we have a call to cacheFieldDisplayNames up in TablePanel to handle this
|
||||
|
||||
@@ -76,9 +76,8 @@ export function TablePanel(props: Props) {
|
||||
onSortByChange={(sortBy) => onSortByChange(sortBy, props)}
|
||||
onColumnResize={(displayName, resizedWidth) => onColumnResize(displayName, resizedWidth, props)}
|
||||
onCellFilterAdded={panelContext.onAddAdHocFilter}
|
||||
footerOptions={options.footer}
|
||||
frozenColumns={options.frozenColumns?.left}
|
||||
enablePagination={options.footer?.enablePagination}
|
||||
enablePagination={options.enablePagination}
|
||||
cellHeight={options.cellHeight}
|
||||
maxRowHeight={options.maxRowHeight}
|
||||
timeRange={timeRange}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createDataFrame, FieldType, PanelModel } from '@grafana/data';
|
||||
import { ByNamesMatcherMode, createDataFrame, FieldMatcherID, FieldType, PanelModel } from '@grafana/data';
|
||||
import { TableCellDisplayMode } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
migrateFooterV2,
|
||||
migrateFromParentRowIndexToNestedFrames,
|
||||
migrateHiddenFields,
|
||||
migrateTextWrapToFieldLevel,
|
||||
@@ -474,4 +475,194 @@ describe('Table Migrations', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateFooterV2', () => {
|
||||
it('is a no-op for panels without footer v1 settings', () => {
|
||||
const panel = {
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: {},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
} as unknown as PanelModel;
|
||||
|
||||
// create a clone pre-migration to compare
|
||||
const origPanel = JSON.parse(JSON.stringify(panel));
|
||||
|
||||
migrateFooterV2(panel);
|
||||
|
||||
// panel should be unchanged
|
||||
expect(origPanel).toEqual(panel);
|
||||
});
|
||||
|
||||
it('migrates a global footer', () => {
|
||||
const panel = {
|
||||
options: {
|
||||
footer: {
|
||||
show: true,
|
||||
reducer: ['sum'],
|
||||
},
|
||||
},
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: {},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
} as unknown as PanelModel;
|
||||
|
||||
migrateFooterV2(panel);
|
||||
|
||||
expect(panel.options.footer).toBeUndefined();
|
||||
expect(panel.fieldConfig.defaults.custom.footer).toEqual({
|
||||
reducers: ['sum'],
|
||||
});
|
||||
});
|
||||
|
||||
it('migrates a field-specific footer', () => {
|
||||
const panel = {
|
||||
options: {
|
||||
footer: {
|
||||
show: true,
|
||||
reducer: ['sum'],
|
||||
fields: ['field1', 'field2'],
|
||||
},
|
||||
},
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: {},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
} as unknown as PanelModel;
|
||||
|
||||
migrateFooterV2(panel);
|
||||
|
||||
expect(panel.options.footer).toBeUndefined();
|
||||
expect(panel.fieldConfig.defaults.custom.footer).toBeUndefined();
|
||||
expect(panel.fieldConfig.overrides).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
matcher: {
|
||||
id: FieldMatcherID.byNames,
|
||||
options: {
|
||||
mode: ByNamesMatcherMode.include,
|
||||
names: ['field1', 'field2'],
|
||||
},
|
||||
},
|
||||
properties: [{ id: 'custom.footer.reducers', value: ['sum'] }],
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('migrates a single-field footer', () => {
|
||||
const panel = {
|
||||
options: {
|
||||
footer: {
|
||||
show: true,
|
||||
reducer: ['sum'],
|
||||
fields: ['field1'],
|
||||
},
|
||||
},
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: {},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
} as unknown as PanelModel;
|
||||
|
||||
migrateFooterV2(panel);
|
||||
|
||||
expect(panel.options.footer).toBeUndefined();
|
||||
expect(panel.fieldConfig.defaults.custom.footer).toBeUndefined();
|
||||
expect(panel.fieldConfig.overrides).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
matcher: {
|
||||
id: FieldMatcherID.byName,
|
||||
options: 'field1',
|
||||
},
|
||||
properties: [{ id: 'custom.footer.reducers', value: ['sum'] }],
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('handles the countAll case', () => {
|
||||
const panel = {
|
||||
options: {
|
||||
footer: {
|
||||
show: true,
|
||||
countRows: true,
|
||||
reducer: ['count'],
|
||||
},
|
||||
},
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: {},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
} as unknown as PanelModel;
|
||||
|
||||
migrateFooterV2(panel);
|
||||
|
||||
expect(panel.options.footer).toBeUndefined();
|
||||
expect(panel.fieldConfig.defaults.custom.footer).toEqual({
|
||||
reducers: ['countAll'],
|
||||
});
|
||||
});
|
||||
|
||||
it('destroys an existing footer if it was hidden', () => {
|
||||
const panel = {
|
||||
options: {
|
||||
footer: {
|
||||
show: false,
|
||||
reducer: ['sum'],
|
||||
fields: ['field1'],
|
||||
},
|
||||
},
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: {},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
} as unknown as PanelModel;
|
||||
|
||||
migrateFooterV2(panel);
|
||||
|
||||
expect(panel.options.footer).toBeUndefined();
|
||||
expect(panel.fieldConfig.defaults.custom.footer).toBeUndefined();
|
||||
expect(panel.fieldConfig.overrides).toEqual([]);
|
||||
});
|
||||
|
||||
it('retains the enablePagination setting if it exists', () => {
|
||||
const panel = {
|
||||
options: {
|
||||
footer: {
|
||||
show: false,
|
||||
reducer: ['sum'],
|
||||
fields: ['field1'],
|
||||
enablePagination: true,
|
||||
},
|
||||
},
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
custom: {},
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
} as unknown as PanelModel;
|
||||
|
||||
migrateFooterV2(panel);
|
||||
|
||||
expect(panel.options.enablePagination).toBe(true);
|
||||
expect(panel.fieldConfig.defaults.custom.footer).toBeUndefined();
|
||||
expect(panel.fieldConfig.overrides).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
FieldConfig,
|
||||
DataFrame,
|
||||
FieldType,
|
||||
ByNamesMatcherMode,
|
||||
} from '@grafana/data';
|
||||
import { ReduceTransformerOptions } from '@grafana/data/internal';
|
||||
|
||||
@@ -27,6 +28,7 @@ export const tableMigrationHandler = (panel: PanelModel<Options>): Partial<Optio
|
||||
|
||||
migrateTextWrapToFieldLevel(panel);
|
||||
migrateHiddenFields(panel);
|
||||
migrateFooterV2(panel);
|
||||
|
||||
// Nothing changed
|
||||
return panel.options;
|
||||
@@ -346,3 +348,66 @@ export const migrateHiddenFields = (panel: PanelModel<Partial<Options>>) => {
|
||||
|
||||
return panel;
|
||||
};
|
||||
|
||||
interface LegacyTableFooterOptions {
|
||||
show: boolean;
|
||||
reducer: string[];
|
||||
fields?: string[];
|
||||
countRows?: boolean;
|
||||
enablePagination?: boolean;
|
||||
}
|
||||
|
||||
export const migrateFooterV2 = (panel: PanelModel<Options>) => {
|
||||
if (panel.options && 'footer' in panel.options) {
|
||||
// we need to cast the footer to the old type to work with it here.
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const oldFooter = panel.options.footer as LegacyTableFooterOptions;
|
||||
|
||||
if (oldFooter.show) {
|
||||
const reducers = oldFooter.reducer;
|
||||
|
||||
panel.fieldConfig.defaults.custom = {
|
||||
...panel.fieldConfig.defaults.custom,
|
||||
footer: {
|
||||
reducers: reducers,
|
||||
},
|
||||
};
|
||||
|
||||
if (oldFooter.countRows && reducers[0] === 'count') {
|
||||
panel.fieldConfig.defaults.custom.footer.reducers = ['countAll'];
|
||||
} else if (oldFooter.fields && oldFooter.fields.length > 1) {
|
||||
delete panel.fieldConfig.defaults.custom.footer;
|
||||
|
||||
// Fields is an array of field names, so push a byNames matcher
|
||||
// on with the matched reducer.
|
||||
panel.fieldConfig.overrides.push({
|
||||
matcher: {
|
||||
id: FieldMatcherID.byNames,
|
||||
options: {
|
||||
mode: ByNamesMatcherMode.include,
|
||||
names: oldFooter.fields,
|
||||
},
|
||||
},
|
||||
properties: [{ id: 'custom.footer.reducers', value: reducers }],
|
||||
});
|
||||
} else if (oldFooter.fields && oldFooter.fields.length === 1) {
|
||||
delete panel.fieldConfig.defaults.custom.footer;
|
||||
|
||||
// Single field, so we can use a byName matcher
|
||||
panel.fieldConfig.overrides.push({
|
||||
matcher: {
|
||||
id: FieldMatcherID.byName,
|
||||
options: oldFooter.fields[0],
|
||||
},
|
||||
properties: [{ id: 'custom.footer.reducers', value: reducers }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (oldFooter.enablePagination != null) {
|
||||
panel.options.enablePagination = oldFooter.enablePagination;
|
||||
}
|
||||
|
||||
delete panel.options.footer;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import {
|
||||
FieldOverrideContext,
|
||||
FieldType,
|
||||
getFieldDisplayName,
|
||||
PanelPlugin,
|
||||
ReducerID,
|
||||
standardEditorsRegistry,
|
||||
identityOverrideProcessor,
|
||||
FieldConfigProperty,
|
||||
} from '@grafana/data';
|
||||
import { PanelPlugin, standardEditorsRegistry, identityOverrideProcessor, FieldConfigProperty } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import {
|
||||
defaultTableFieldOptions,
|
||||
@@ -75,6 +66,45 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(TablePanel)
|
||||
},
|
||||
defaultValue: defaultTableFieldOptions.align,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'filterable',
|
||||
name: t('table.name-column-filter', 'Column filter'),
|
||||
category,
|
||||
description: t('table.description-column-filter', 'Enables/disables field filters in table'),
|
||||
defaultValue: defaultTableFieldOptions.filterable,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'wrapText',
|
||||
name: t('table.name-wrap-text', 'Wrap text'),
|
||||
category,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'wrapHeaderText',
|
||||
name: t('table.name-wrap-header-text', 'Wrap header text'),
|
||||
category,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'hideFrom.viz',
|
||||
name: t('table.name-hide-in-table', 'Hide in table'),
|
||||
category,
|
||||
defaultValue: undefined,
|
||||
hideFromDefaults: true,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'footer.reducers',
|
||||
category: [t('table.category-table-footer', 'Table footer')],
|
||||
path: 'footer.reducers',
|
||||
name: t('table.name-calculation', 'Calculation'),
|
||||
description: t('table.description-calculation', 'Choose a reducer function / calculation'),
|
||||
editor: standardEditorsRegistry.get('stats-picker').editor,
|
||||
override: standardEditorsRegistry.get('stats-picker').editor,
|
||||
defaultValue: [],
|
||||
process: identityOverrideProcessor,
|
||||
shouldApply: () => true,
|
||||
settings: {
|
||||
allowMultiple: true,
|
||||
},
|
||||
})
|
||||
.addCustomEditor<void, TableCellOptions>({
|
||||
id: 'cellOptions',
|
||||
path: 'cellOptions',
|
||||
@@ -101,30 +131,6 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(TablePanel)
|
||||
);
|
||||
},
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'filterable',
|
||||
name: t('table.name-column-filter', 'Column filter'),
|
||||
category,
|
||||
description: t('table.description-column-filter', 'Enables/disables field filters in table'),
|
||||
defaultValue: defaultTableFieldOptions.filterable,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'wrapText',
|
||||
name: t('table.name-wrap-text', 'Wrap text'),
|
||||
category,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'wrapHeaderText',
|
||||
name: t('table.name-wrap-header-text', 'Wrap header text'),
|
||||
category,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'hideFrom.viz',
|
||||
name: t('table.name-hide-in-table', 'Hide in table'),
|
||||
category,
|
||||
defaultValue: undefined,
|
||||
hideFromDefaults: true,
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'tooltip.field',
|
||||
name: t('table.name-tooltip-from-field', 'Tooltip from field'),
|
||||
@@ -167,7 +173,6 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(TablePanel)
|
||||
},
|
||||
})
|
||||
.setPanelOptions((builder) => {
|
||||
const footerCategory = [t('table.category-table-footer', 'Table footer')];
|
||||
const category = [t('table.category-table', 'Table')];
|
||||
builder
|
||||
.addBooleanSwitch({
|
||||
@@ -207,63 +212,13 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(TablePanel)
|
||||
min: 0,
|
||||
},
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'footer.show',
|
||||
category: footerCategory,
|
||||
name: t('table.name-show-table-footer', 'Show table footer'),
|
||||
defaultValue: defaultOptions.footer?.show,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'footer.reducer',
|
||||
category: footerCategory,
|
||||
path: 'footer.reducer',
|
||||
name: t('table.name-calculation', 'Calculation'),
|
||||
description: t('table.description-calculation', 'Choose a reducer function / calculation'),
|
||||
editor: standardEditorsRegistry.get('stats-picker').editor,
|
||||
defaultValue: [ReducerID.sum],
|
||||
showIf: (cfg) => cfg.footer?.show,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'footer.countRows',
|
||||
category: footerCategory,
|
||||
name: t('table.name-count-rows', 'Count rows'),
|
||||
description: t('table.description-count-rows', 'Display a single count for all data rows'),
|
||||
defaultValue: defaultOptions.footer?.countRows,
|
||||
showIf: (cfg) => cfg.footer?.reducer?.length === 1 && cfg.footer?.reducer[0] === ReducerID.count,
|
||||
})
|
||||
.addMultiSelect({
|
||||
path: 'footer.fields',
|
||||
category: footerCategory,
|
||||
name: t('table.name-fields', 'Fields'),
|
||||
description: t('table.description-fields', 'Select the fields that should be calculated'),
|
||||
settings: {
|
||||
allowCustomValue: false,
|
||||
options: [],
|
||||
placeholder: t('table.placeholder-fields', 'All Numeric Fields'),
|
||||
getOptions: async (context: FieldOverrideContext) => {
|
||||
const options = [];
|
||||
if (context && context.data && context.data.length > 0) {
|
||||
const frame = context.data[0];
|
||||
for (const field of frame.fields) {
|
||||
if (field.type === FieldType.number) {
|
||||
const name = getFieldDisplayName(field, frame, context.data);
|
||||
const value = field.name;
|
||||
options.push({ value, label: name });
|
||||
}
|
||||
}
|
||||
}
|
||||
return options;
|
||||
},
|
||||
},
|
||||
defaultValue: '',
|
||||
showIf: (cfg) => cfg.footer?.show && !(cfg.footer?.countRows && cfg.footer?.reducer.includes(ReducerID.count)),
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'footer.enablePagination',
|
||||
path: 'footer.enablePagination',
|
||||
id: 'enablePagination',
|
||||
path: 'enablePagination',
|
||||
name: t('table.name-enable-pagination', 'Enable pagination'),
|
||||
category,
|
||||
editor: PaginationEditor,
|
||||
defaultValue: defaultOptions?.enablePagination,
|
||||
});
|
||||
})
|
||||
.setSuggestionsSupplier(new TableSuggestionsSupplier());
|
||||
|
||||
@@ -33,15 +33,8 @@ composableKinds: PanelCfg: {
|
||||
showTypeIcons?: bool | *false
|
||||
// Used to control row sorting
|
||||
sortBy?: [...ui.TableSortByFieldState]
|
||||
// Controls footer options
|
||||
footer?: ui.TableFooterOptions | *{
|
||||
// Controls whether the footer should be shown
|
||||
show: false
|
||||
// Controls whether the footer should show the total number of rows on Count calculation
|
||||
countRows: false
|
||||
// Represents the selected calculations
|
||||
reducer: []
|
||||
}
|
||||
// Enable pagination on the table
|
||||
enablePagination?: bool
|
||||
// Controls the height of the rows
|
||||
cellHeight?: ui.TableCellHeight & (*"sm" | _)
|
||||
// limits the maximum height of a row, if text wrapping or dynamic height is enabled
|
||||
|
||||
@@ -16,9 +16,9 @@ export interface Options {
|
||||
*/
|
||||
cellHeight?: ui.TableCellHeight;
|
||||
/**
|
||||
* Controls footer options
|
||||
* Enable pagination on the table
|
||||
*/
|
||||
footer?: ui.TableFooterOptions;
|
||||
enablePagination?: boolean;
|
||||
/**
|
||||
* Represents the index of the selected frame
|
||||
*/
|
||||
@@ -49,20 +49,6 @@ export interface Options {
|
||||
|
||||
export const defaultOptions: Partial<Options> = {
|
||||
cellHeight: ui.TableCellHeight.Sm,
|
||||
footer: {
|
||||
/**
|
||||
* Controls whether the footer should be shown
|
||||
*/
|
||||
show: false,
|
||||
/**
|
||||
* Controls whether the footer should show the total number of rows on Count calculation
|
||||
*/
|
||||
countRows: false,
|
||||
/**
|
||||
* Represents the selected calculations
|
||||
*/
|
||||
reducer: [],
|
||||
},
|
||||
frameIndex: 0,
|
||||
showHeader: true,
|
||||
showTypeIcons: false,
|
||||
|
||||
@@ -8772,7 +8772,6 @@
|
||||
"cell-inspect": "Inspect value",
|
||||
"cell-inspect-tooltip": "Inspect value",
|
||||
"copy": "Copy to Clipboard",
|
||||
"count": "Count",
|
||||
"csv-counts": "Rows:{{rows}}, Columns:{{columns}} <5></5>",
|
||||
"csv-placeholder": "Enter CSV here...",
|
||||
"filter-placeholder": "Filter values",
|
||||
@@ -8783,6 +8782,11 @@
|
||||
"filter-popup-heading": "Filter by values:",
|
||||
"filter-popup-input-placeholder": "Filter values",
|
||||
"filter-popup-match-case": "Match case",
|
||||
"footer": {
|
||||
"reducer": {
|
||||
"count": "Count"
|
||||
}
|
||||
},
|
||||
"inspect-drawer-title": "Inspect value",
|
||||
"nested-table": {
|
||||
"no-data": "No data"
|
||||
@@ -12788,8 +12792,6 @@
|
||||
"description-calculation": "Choose a reducer function / calculation",
|
||||
"description-cell-value-inspect": "Enable cell value inspection in a modal window",
|
||||
"description-column-filter": "Enables/disables field filters in table",
|
||||
"description-count-rows": "Display a single count for all data rows",
|
||||
"description-fields": "Select the fields that should be calculated",
|
||||
"description-frozen-columns": "Columns are frozen from the left side of the table",
|
||||
"description-min-column-width": "The minimum width for column auto resizing",
|
||||
"description-tooltip-from-field": "Render a cell from a field (hidden or visible) in a tooltip",
|
||||
@@ -12816,21 +12818,17 @@
|
||||
"name-column-alignment": "Column alignment",
|
||||
"name-column-filter": "Column filter",
|
||||
"name-column-width": "Column width",
|
||||
"name-count-rows": "Count rows",
|
||||
"name-enable-pagination": "Enable pagination",
|
||||
"name-fields": "Fields",
|
||||
"name-frozen-columns": "Frozen columns",
|
||||
"name-hide-in-table": "Hide in table",
|
||||
"name-max-height": "Max row height",
|
||||
"name-min-column-width": "Minimum column width",
|
||||
"name-show-table-footer": "Show table footer",
|
||||
"name-show-table-header": "Show table header",
|
||||
"name-tooltip-from-field": "Tooltip from field",
|
||||
"name-tooltip-placement": "Tooltip placement",
|
||||
"name-wrap-header-text": "Wrap header text",
|
||||
"name-wrap-text": "Wrap text",
|
||||
"placeholder-column-width": "auto",
|
||||
"placeholder-fields": "All Numeric Fields",
|
||||
"placeholder-max-height": "none",
|
||||
"tooltip-placement-options": {
|
||||
"label-auto": "Auto",
|
||||
|
||||
Reference in New Issue
Block a user