From cc64b1748324fb42f9b6154524ac8315cd9fd5d4 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Thu, 7 Aug 2025 19:53:52 -0400 Subject: [PATCH] Table: Move cell-specific styles out to their own methods (#108941) * TableNG: Markdown cell, plus custom row height * tab indentation in cue file * fix i18n * trying an auto height with the updated RDG * get auto cellHeight working * i18n updates * hoor disable_sanitize_html flag in MarkdownCell * update react-data-grid version to attempt to support page up and down * removing custom height * use the latest experimental RDG with paging up and down * TableNG: Wrap text for DataLinks and Pills; groundwork for max wrap length * disable editing max wrapped lines for now * disable wrap text line limit e2e * new i18n extract after commenting out input * wip * kill max wrapped lines for now * more cleanup * remove targeting classes added for max wrapped lines * fix Pill test * couple more style cleanups * Table: Move cell-specific styles out to their own methods * move styles into their own file * combine renderer and style declarations to make auto cells work better, complete cleanup of internal cell elements * fix e2es given these updates * add a couple tests * wip: tests * add tests * bump up capital letters in lorem ipsum * fix copy-pasta mistake * whoops, mis-merged the selector * use a local count instead of getCellLinks * use react-data-grid on react-18 branch * fix linting on test * gdev dashboard and smoketest for Markdown table * remove cellHeightCustom * restore bugfix from adversarial AI-generated JSON * reorganize in light of recent and upcoming changes * cleanup * override the whitespace for markdown * what are these auto imports about... * fix cell height selector from merge * also remove cellHeightCustom * i18n * avoid the important override in markdown cell styles * revert some betterer config autoformatting * slight code cleanup * s/cat/grot, add color link panel to kitchen sink, fix color link/image link style issues * update panelid for empty table panel test * link styles outside of cell style setup * flesh out kitchen sink examples, update ImageCell and applyToRow * clean up some inconsistent states * fix lint issue * gdev update * format JSON to satisfy linter * shortening the text in the long text field --------- Co-authored-by: Leon Sorokin --- .betterer.eslint.config.js | 19 + .../panel-table/table_kitchen_sink.json | 1254 ++++++++++++++++- .../panel-table/table_markdown.json | 2 +- .../panels-suite/table-kitchenSink.spec.ts | 33 +- .../grafana-schema/src/common/common.gen.ts | 45 +- packages/grafana-schema/src/common/table.cue | 12 +- .../Table/TableNG/Cells/ActionsCell.tsx | 27 +- .../Table/TableNG/Cells/AutoCell.tsx | 27 +- .../Table/TableNG/Cells/BarGaugeCell.tsx | 9 +- .../Table/TableNG/Cells/DataLinksCell.tsx | 29 +- .../Table/TableNG/Cells/GeoCell.tsx | 31 +- .../Table/TableNG/Cells/ImageCell.tsx | 38 +- .../Table/TableNG/Cells/MarkdownCell.tsx | 30 +- .../Table/TableNG/Cells/PillCell.tsx | 25 +- .../Table/TableNG/Cells/SparklineCell.tsx | 42 +- .../Table/TableNG/Cells/renderers.test.tsx | 7 +- .../Table/TableNG/Cells/renderers.tsx | 153 +- .../src/components/Table/TableNG/TableNG.tsx | 321 +---- .../{Cells => components}/FooterCell.tsx | 0 .../{Cells => components}/HeaderCell.tsx | 5 +- .../{Cells => components}/RowExpander.tsx | 0 .../TableCellActions.tsx | 0 .../src/components/Table/TableNG/constants.ts | 1 - .../src/components/Table/TableNG/styles.ts | 158 +++ .../src/components/Table/TableNG/types.ts | 16 +- 25 files changed, 1738 insertions(+), 546 deletions(-) rename packages/grafana-ui/src/components/Table/TableNG/{Cells => components}/FooterCell.tsx (100%) rename packages/grafana-ui/src/components/Table/TableNG/{Cells => components}/HeaderCell.tsx (96%) rename packages/grafana-ui/src/components/Table/TableNG/{Cells => components}/RowExpander.tsx (100%) rename packages/grafana-ui/src/components/Table/TableNG/{Cells => components}/TableCellActions.tsx (100%) create mode 100644 packages/grafana-ui/src/components/Table/TableNG/styles.ts diff --git a/.betterer.eslint.config.js b/.betterer.eslint.config.js index c1a38731291..ae8b600826f 100644 --- a/.betterer.eslint.config.js +++ b/.betterer.eslint.config.js @@ -157,4 +157,23 @@ module.exports = [ 'no-barrel-files/no-barrel-files': 'error', }, }, + { + // custom rule for Table to avoid performance regressions + files: ['packages/grafana-ui/src/components/Table/TableNG/Cells/**/*.{ts,tsx}'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['**/themes/ThemeContext'], + importNames: ['useStyles2', 'useTheme2'], + message: + 'Do not use "useStyles2" or "useTheme2" in a cell directly. Instead, provide styles to cells via `getDefaultCellStyles` or `getCellSpecificStyles`.', + }, + ], + }, + ], + }, + }, ]; diff --git a/devenv/dev-dashboards/panel-table/table_kitchen_sink.json b/devenv/dev-dashboards/panel-table/table_kitchen_sink.json index 9c05634b066..77ba090a0df 100644 --- a/devenv/dev-dashboards/panel-table/table_kitchen_sink.json +++ b/devenv/dev-dashboards/panel-table/table_kitchen_sink.json @@ -18,7 +18,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 138, + "id": 21409, "links": [], "panels": [ { @@ -78,6 +78,10 @@ { "id": "max", "value": 100 + }, + { + "id": "custom.inspect", + "value": true } ] }, @@ -140,14 +144,14 @@ }, { "matcher": { - "id": "byName", - "options": "Image" + "id": "byRegexp", + "options": "/Image/" }, "properties": [ { "id": "custom.cellOptions", "value": { - "alt": "${__value}", + "alt": "Grafana icon", "type": "image" } } @@ -278,7 +282,7 @@ }, { "id": "custom.width", - "value": 300 + "value": 255 } ] }, @@ -291,7 +295,8 @@ { "id": "custom.cellOptions", "value": { - "type": "pill" + "type": "pill", + "wrapText": true } }, { @@ -309,7 +314,8 @@ { "id": "custom.cellOptions", "value": { - "type": "data-links" + "type": "data-links", + "wrapText": true } }, { @@ -329,7 +335,7 @@ }, { "id": "custom.width", - "value": 180 + "value": 139 } ] }, @@ -339,12 +345,65 @@ "options": "Gauge" }, "properties": [] + }, + { + "matcher": { + "id": "byName", + "options": "Action" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "actions" + } + }, + { + "id": "actions", + "value": [ + { + "confirmation": "", + "fetch": { + "body": "{}", + "headers": [["Content-Type", "application/json"]], + "method": "GET", + "queryParams": [], + "url": "/api/health" + }, + "title": "Get instance health", + "type": "fetch" + } + ] + }, + { + "id": "custom.width", + "value": 142 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Image w/ Link" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Grafana Cloud Products", + "url": "https://grafana.com/products/cloud/" + } + ] + } + ] } ] }, "gridPos": { - "h": 15, - "w": 24, + "h": 12, + "w": 16, "x": 0, "y": 0 }, @@ -360,9 +419,14 @@ }, "frameIndex": 0, "showHeader": true, - "sortBy": [] + "sortBy": [ + { + "desc": false, + "displayName": "Long Text" + } + ] }, - "pluginVersion": "12.1.0-pre", + "pluginVersion": "12.2.0-pre", "targets": [ { "datasource": { @@ -372,7 +436,7 @@ "scenarioId": "random_walk_table" }, { - "csvContent": "Info,Image,Pills,Data Link,Long Text\ndown,https://placecats.com/millie/200/400,hello,https://grafana.com,\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse tempus et augue et lacinia. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec eu pretium tortor. Cras venenatis sapien sed mauris gravida, ut scelerisque est fringilla. Cras lorem diam, facilisis nec malesuada in, vulputate vel enim. Etiam fringilla nisi quis felis blandit tincidunt. Cras id lacus ornare, ullamcorper nisl eget, bibendum odio. Pellentesque imperdiet, leo a imperdiet venenatis, ligula risus venenatis quam, vel euismod magna nisi sit amet leo.\"\nup,https://placecats.com/neo/200/400,\"[1,2,3,\"\"foo\"\",\"\"bar\"\"]\",https://grafana.com/solutions/kubernetes/,\"Sed imperdiet eget diam sit amet fringilla. Curabitur quis lacus blandit, mollis diam non, accumsan tortor. Aliquam ac tellus eget dui facilisis tempor eu id nulla. Maecenas ultrices turpis eu elementum imperdiet. Fusce eget rhoncus mi, et egestas lectus. Mauris facilisis auctor enim sed malesuada. Maecenas placerat ultricies metus vitae viverra. In hac habitasse platea dictumst. Mauris ipsum nisl, dictum eu aliquam eleifend, rutrum id orci. Nullam eget dui et odio eleifend porttitor.\"\nup fast,https://placecats.com/bella/200/400,\"foo,1,4,beep\",https://k6.io/,\"Proin ac libero vulputate ex vulputate pharetra ut vel lacus. Phasellus quis dolor sed leo finibus scelerisque. Ut vel finibus leo, sed viverra ipsum. Suspendisse vitae rutrum arcu. Donec sed tellus vel lectus bibendum vestibulum. Sed eu felis non velit dictum pulvinar eu et leo. Aenean et dignissim arcu. Nam luctus at neque quis efficitur. Fusce tempus at nibh a imperdiet. Nullam malesuada ac magna at facilisis. Duis pretium aliquam eros. Donec pharetra dignissim dolor non bibendum. Ut gravida mi id urna tempus, at ullamcorper felis vulputate. Duis congue augue ex, sed finibus leo ornare ut. Mauris non quam sodales, dignissim lorem eget, tincidunt mauris. Aliquam ut velit auctor, vestibulum metus sed, mollis massa.\"\ndown fast,https://placecats.com/neo_2/200/400,\"foo,bar,baz,a longer one,bim\",https://grafana.com/products/cloud/,\"Nullam in pulvinar justo. Nunc dictum arcu ac pellentesque bibendum. Sed in erat turpis. Vestibulum eu orci ac ligula lobortis tempus. Fusce consectetur feugiat magna, eu tempor nibh vestibulum ac. Aliquam erat volutpat. Vivamus sit amet viverra enim. Quisque mollis odio nulla, nec vulputate sem placerat in. Etiam dolor sapien, pulvinar in accumsan at, consequat eget nisi. Nunc condimentum neque magna, congue consectetur dui efficitur interdum. Nam lobortis fringilla maximus. Vestibulum eu dui a velit condimentum eleifend consequat nec lectus.\"", + "csvContent": "Info,Image,Image w/ Link,Pills,Data Link,Long Text\ndown,https://grafana.com/media/menus/products/grafana-menu-icon.svg,https://grafana.com/media/menus/products/grafana-menu-icon.svg,hello,https://grafana.com,\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse tempus et augue et lacinia. Interdum et malesuada fames ac ante ipsum primis in faucibus.\"\nup,https://grafana.com/media/menus/products/grafana-menu-icon-logs.svg,https://grafana.com/media/menus/products/grafana-menu-icon-logs.svg,\"[1,2,3,\"\"foo\"\",\"\"bar\"\"]\",https://grafana.com/solutions/kubernetes/,\"Sed imperdiet eget diam sit amet fringilla. Curabitur quis lacus blandit, mollis diam non, accumsan tortor.\"\nup fast,https://grafana.com/media/menus/products/grafana-menu-icon-traces.svg,https://grafana.com/media/menus/products/grafana-menu-icon-traces.svg,\"foo,1,4,beep\",https://k6.io/,\"Proin ac libero vulputate ex vulputate pharetra ut vel lacus. Phasellus quis dolor sed leo finibus scelerisque. Ut vel finibus leo, sed viverra ipsum.\"\ndown fast,https://grafana.com/media/menus/products/grafana-menu-icon-metrics.svg,https://grafana.com/media/menus/products/grafana-menu-icon-metrics.svg,\"foo,bar,baz,a longer one,bim\",https://grafana.com/products/cloud/,\"Nullam in pulvinar justo. Nunc dictum arcu ac pellentesque bibendum. Sed in erat turpis. Vestibulum eu orci ac ligula lobortis tempus.\"", "datasource": { "type": "grafana-testdata-datasource" }, @@ -389,6 +453,30 @@ "mode": "outerTabular" } }, + { + "id": "calculateField", + "options": { + "alias": "Action", + "binary": { + "left": { + "matcher": { + "id": "byName", + "options": "Image" + } + }, + "right": { + "matcher": { + "id": "byName", + "options": "$Filters" + } + } + }, + "mode": "index", + "reduce": { + "reducer": "sum" + } + } + }, { "id": "organize", "options": { @@ -397,14 +485,16 @@ }, "includeByName": {}, "indexByName": { - "A": 9, - "Data Link": 8, + "A": 11, + "Action": 7, + "Data Link": 10, "Image": 5, + "Image w/ Link": 6, "Info": 1, - "Long Text": 7, + "Long Text": 9, "Max A": 3, "Min A": 2, - "Pills": 6, + "Pills": 8, "State A": 4, "Time A": 0 }, @@ -426,17 +516,1082 @@ "type": "table" }, { - "id": 2, - "type": "table", - "title": "Empty Table Panel", - "gridPos": { - "x": 0, - "y": 0, - "h": 6, - "w": 24 + "datasource": { + "type": "grafana-testdata-datasource" }, "fieldConfig": { "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "color-background" + }, + "filterable": true, + "inspect": true, + "wrapHeaderText": false + }, + "fieldMinMax": true, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 4, + "maxDataPoints": 100, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": ["max"], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "State" + } + ] + }, + "pluginVersion": "12.2.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "random_walk_table" + } + ], + "title": "Nested tables", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "A": false + }, + "includeByName": {}, + "indexByName": { + "A": 5, + "Info": 2, + "Max": 4, + "Min": 3, + "State": 1, + "Time": 0 + }, + "renameByName": {} + } + }, + { + "id": "groupToNestedTable", + "options": { + "fields": { + "State": { + "aggregations": [], + "operation": "groupby" + } + }, + "showSubframeHeaders": true + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false, + "minWidth": 50, + "wrapHeaderText": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 3 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/\\b(bg|color)\\b/" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "a": { + "color": "dark-red", + "index": 0 + }, + "b": { + "color": "super-light-orange", + "index": 1 + }, + "c": { + "color": "green", + "index": 2 + }, + "d": { + "color": "super-light-blue", + "index": 3 + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/\\bcolor\\b/" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/\\bbg\\b/" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/\\blink\\b/" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "goto foo", + "url": "https://foo" + } + ] + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/\\blinks\\b/" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "goto foo", + "url": "https://foo" + }, + { + "targetBlank": true, + "title": "goto bar", + "url": "https://bar" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "links-cellType" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "data-links" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.2.0-pre", + "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", + "datasource": { + "type": "grafana-testdata-datasource" + }, + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "Colors and Links", + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false, + "wrapHeaderText": false + }, + "fieldMinMax": true, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "applyToRow": true, + "mode": "basic", + "type": "color-background" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-greens" + } + }, + { + "id": "custom.width", + "value": 50 + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/(Min|Max)/" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Info" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Google this term", + "url": "https://google.com/search?q=${__value.raw}" + } + ] + }, + { + "id": "custom.width", + "value": 79 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 156 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "State" + }, + "properties": [ + { + "id": "custom.width", + "value": 99 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 0, + "y": 19 + }, + "id": 6, + "maxDataPoints": 10, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.2.0-pre", + "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]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "Apply to row - basic", + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false, + "wrapHeaderText": false + }, + "fieldMinMax": true, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "applyToRow": true, + "type": "color-background" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-blues" + } + }, + { + "id": "custom.width", + "value": 50 + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/(Min|Max)/" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Info" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Google this term", + "url": "https://google.com/search?q=${__value.raw}" + } + ] + }, + { + "id": "custom.width", + "value": 59 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 161 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "State" + }, + "properties": [ + { + "id": "custom.width", + "value": 114 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 4, + "y": 19 + }, + "id": 5, + "maxDataPoints": 10, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.2.0-pre", + "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]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "Apply to row - gradient", + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false, + "wrapHeaderText": false + }, + "fieldMinMax": true, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "applyToRow": true, + "type": "color-background" + } + }, + { + "id": "color", + "value": { + "mode": "thresholds" + } + }, + { + "id": "custom.width", + "value": 50 + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/(Min|Max)/" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Info" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Google this term", + "url": "https://google.com/search?q=${__value.raw}" + } + ] + }, + { + "id": "custom.width", + "value": 79 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 158 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "State" + }, + "properties": [ + { + "id": "custom.width", + "value": 97 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 8, + "y": 19 + }, + "id": 7, + "maxDataPoints": 10, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.2.0-pre", + "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]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "Apply to row - thresholds", + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false, + "wrapHeaderText": false + }, + "fieldMinMax": true, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A" + }, + "properties": [ + { + "id": "custom.width", + "value": 50 + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/(Min|Max)/" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Info" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Google this term", + "url": "https://google.com/search?q=${__value.raw}" + } + ] + }, + { + "id": "custom.width", + "value": 79 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 158 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "State" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "applyToRow": true, + "type": "color-background" + } + }, + { + "id": "custom.width", + "value": 97 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 12, + "y": 19 + }, + "id": 8, + "maxDataPoints": 10, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "A" + } + ] + }, + "pluginVersion": "12.2.0-pre", + "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]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "Apply to row - enum", + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false, + "wrapHeaderText": false + }, + "fieldMinMax": true, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "palette-classic" + } + }, + { + "id": "custom.width", + "value": 50 + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/(Min|Max)/" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Info" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Google this term", + "url": "https://google.com/search?q=${__value.raw}" + } + ] + }, + { + "id": "custom.width", + "value": 79 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.width", + "value": 158 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "A" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "from": 0, + "result": { + "color": "purple", + "index": 0 + }, + "to": 76.999 + }, + "type": "range" + }, + { + "options": { + "from": 77, + "result": { + "color": "orange", + "index": 1 + } + }, + "type": "range" + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "applyToRow": true, + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "State" + }, + "properties": [ + { + "id": "custom.width", + "value": 97 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 16, + "y": 19 + }, + "id": 9, + "maxDataPoints": 10, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "12.2.0-pre", + "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]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "title": "Apply to row - mappings", + "type": "table" + }, + { + "datasource": { + "type": "grafana-testdata-datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, "custom": { "align": "auto", "cellOptions": { @@ -450,42 +1605,45 @@ "mode": "absolute", "steps": [ { - "value": null, - "color": "green" + "color": "green", + "value": 0 }, { - "value": 80, - "color": "red" + "color": "red", + "value": 80 } ] - }, - "color": { - "mode": "thresholds" } }, "overrides": [] }, - "pluginVersion": "12.1.0-pre", - "targets": [ - { - "scenarioId": "csv_content", - "refId": "A", - "csvContent": "a,b,c" - } - ], - "datasource": { - "type": "grafana-testdata-datasource" + "gridPos": { + "h": 7, + "w": 4, + "x": 20, + "y": 19 }, + "id": 3, "options": { - "showHeader": true, "cellHeight": "sm", "footer": { - "show": false, - "reducer": ["sum"], "countRows": false, - "fields": "" + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.2.0-pre", + "targets": [ + { + "csvContent": "a,b,c", + "refId": "A", + "scenarioId": "csv_content" } - } + ], + "title": "Empty Table Panel", + "type": "table" } ], "preload": false, @@ -512,5 +1670,5 @@ "timezone": "", "title": "Panel Tests - Table - Kitchen Sink", "uid": "dcb9f5e9-8066-4397-889e-864b99555dbb", - "version": 2 + "version": 7 } diff --git a/devenv/dev-dashboards/panel-table/table_markdown.json b/devenv/dev-dashboards/panel-table/table_markdown.json index 863edd09fed..06f3d7c310c 100644 --- a/devenv/dev-dashboards/panel-table/table_markdown.json +++ b/devenv/dev-dashboards/panel-table/table_markdown.json @@ -93,7 +93,7 @@ "pluginVersion": "12.1.0-pre", "targets": [ { - "csvContent": "id,markdown\n1,\"
\n
Definition list
\n
Is something people use sometimes.
\n
Markdown in HTML
\n
Does *not* work **very** well. Use HTML tags.
\n
\"\n2,\"Three or more...\n\n---\n\nHyphens\n\n***\n\nAsterisks\n\n___\n\nUnderscores\"\n3,\"Here's a line for us to start with.\n\nThis line is separated from the one above by two newlines, so it will be a *separate paragraph*.\n\nThis line is also a separate paragraph, but...\nThis line is only separated by a single newline, so it's a separate line in the *same paragraph*.\"\n4,\"red, green, blue\"\n5,\"\"\n6,\"[Link](https://grafana.com), or HTML link\"\n7,\"1. foo\n1. bar\n - baz\n * bim\n3. bip\"\n8,\"# heading 1\n## heading 2\n### heading 3\n#### heading 4\n##### heading 5\n###### heading 6\"\n9,\"Emphasis, aka italics, with *asterisks* or _underscores_.\n\nStrong emphasis, aka bold, with **asterisks** or __underscores__.\n\nCombined emphasis with **asterisks and _underscores_**.\n\nStrikethrough uses two tildes. ~~Scratch this.~~\n\nunderline does require an HTML element tho.\"\n10,\"```javascript\nvar s = 'JavaScript syntax highlighting';\nalert(s);\n```\"\n11,\"\n| Month | Savings |\n| -------- | ------- |\n| I heard | $250 |\n| you like | $80 |\n| tables | $365 |\"\n", + "csvContent": "id,markdown\n1,\"
\n
Definition list
\n
Is something people use sometimes.
\n
Markdown in HTML
\n
Does *not* work **very** well. Use HTML tags.
\n
\"\n2,\"Three or more...\n\n---\n\nHyphens\n\n***\n\nAsterisks\n\n___\n\nUnderscores\"\n3,\"Here's a line for us to start with.\n\nThis line is separated from the one above by two newlines, so it will be a *separate paragraph*.\n\nThis line is also a separate paragraph, but...\nThis line is only separated by a single newline, so it's a separate line in the *same paragraph*.\"\n4,\"red, green, blue\"\n5,\"\"\n6,\"[Link](https://grafana.com), or HTML link\"\n7,\"1. foo\n1. bar\n - baz\n * bim\n3. bip\"\n8,\"# heading 1\n## heading 2\n### heading 3\n#### heading 4\n##### heading 5\n###### heading 6\"\n9,\"Emphasis, aka italics, with *asterisks* or _underscores_.\n\nStrong emphasis, aka bold, with **asterisks** or __underscores__.\n\nCombined emphasis with **asterisks and _underscores_**.\n\nStrikethrough uses two tildes. ~~Scratch this.~~\n\nunderline does require an HTML element tho.\"\n10,\"```javascript\nvar s = 'JavaScript syntax highlighting';\nalert(s);\n```\"\n11,\"\n| Month | Savings |\n| -------- | ------- |\n| I heard | $250 |\n| you like | $80 |\n| tables | $365 |\"\n", "datasource": { "type": "grafana-testdata-datasource", "uid": "gdev-testdata" diff --git a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts index 8ac43ceebb1..5ddb7c4d4e4 100644 --- a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts +++ b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @@ -1,6 +1,6 @@ import { Page, Locator } from '@playwright/test'; -import { test, expect } from '@grafana/plugin-e2e'; +import { test, expect, E2ESelectorGroups } from '@grafana/plugin-e2e'; const DASHBOARD_UID = 'dcb9f5e9-8066-4397-889e-864b99555dbb'; @@ -41,6 +41,21 @@ const getColumnIdx = async (loc: Page | Locator, columnName: string) => { 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.locator( + `[aria-label="${selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text')}"]` + ); + const count = await wrapTextToggle.count(); + + for (let i = 0; i < count; i++) { + const toggle = wrapTextToggle.nth(i); + if ((await toggle.locator('//preceding-sibling::input').getAttribute('checked')) !== null) { + await toggle.click(); + } + } +}; + test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] }, () => { test('Tests word wrap, hover overflow, and cell inspect', async ({ gotoDashboardPage, selectors, page }) => { const dashboardPage = await gotoDashboardPage({ @@ -214,11 +229,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] // because of text wrapping, we're guaranteed to only be showing a single row when we enable pagination. await expect(page.getByText(/([\d]+) - ([\d]+) of ([\d]+) rows/)).toBeVisible(); - // FIXME horrible selector for the "Wrap text" toggle for the "Long text" column. - await page - .locator('[id="Override 13"]') - .locator(`[aria-label="${selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text')}"]`) - .click(); + await disableAllTextWrap(page, selectors); // any number of rows that is not "1" is allowed here, we don't want to police the exact number of rows that // are rendered since there are tons of factors which could effect this. we do want to grab this number for comparison @@ -281,13 +292,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink')) ).toBeVisible(); - // disable text wrapping for this test to make it easier to click the links, the long lorem ipsum - // can push the links off the screen. - // FIXME very bad selector to get the correct "wrap text" toggle here. - await page - .locator('[id="Override 13"]') - .locator(`[aria-label="${selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text')}"]`) - .click(); + await disableAllTextWrap(page, selectors); const infoColumnIdx = await getColumnIdx(page, 'Info'); const pillColIdx = await getColumnIdx(page, 'Pills'); @@ -351,7 +356,7 @@ test.describe('Panels test: Table - Kitchen Sink', { tag: ['@panels', '@table'] test('Empty Table panel', async ({ gotoDashboardPage, selectors }) => { const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID, - queryParams: new URLSearchParams({ editPanel: '2' }), + queryParams: new URLSearchParams({ editPanel: '3' }), }); await expect( diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index c256e8ed6bd..62346b070f6 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -704,6 +704,7 @@ export enum TableCellDisplayMode { Custom = 'custom', DataLinks = 'data-links', Gauge = 'gauge', + Geo = 'geo', GradientGauge = 'gradient-gauge', Image = 'image', JSONView = 'json-view', @@ -837,27 +838,6 @@ export interface TablePillCellOptions extends TableWrapTextOptions { type: TableCellDisplayMode.Pill; } -export interface TableMarkdownCellOptions { - dynamicHeight?: boolean; - type: TableCellDisplayMode.Markdown; -} - -/** - * Height of a table cell - */ -export enum TableCellHeight { - Auto = 'auto', - Lg = 'lg', - Md = 'md', - Sm = 'sm', -} - -/** - * Table cell options. Each cell has a display mode - * and other potential options for that display. - */ -export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions | TableMarkdownCellOptions); - /** * Use UTC/GMT timezone */ @@ -973,6 +953,29 @@ export enum ComparisonOperation { NEQ = 'neq', } +export interface TableMarkdownCellOptions { + dynamicHeight?: boolean; + type: TableCellDisplayMode.Markdown; +} + +/** + * Height of a table cell + */ +export enum TableCellHeight { + Auto = 'auto', + Lg = 'lg', + Md = 'md', + Sm = 'sm', +} + +/** + * Table cell options. Each cell has a display mode + * and other potential options for that display. + */ +export type TableCellOptions = (TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions | TableMarkdownCellOptions | { + type: TableCellDisplayMode.Geo + }); + /** * Field options for each field within a table (e.g 10, "The String", 64.20, etc.) * Generally defines alignment, filtering capabilties, display options, etc. diff --git a/packages/grafana-schema/src/common/table.cue b/packages/grafana-schema/src/common/table.cue index 57b7b56dc44..9fe71322a31 100644 --- a/packages/grafana-schema/src/common/table.cue +++ b/packages/grafana-schema/src/common/table.cue @@ -4,7 +4,7 @@ package common // in the table such as colored text, JSON, gauge, etc. // The color-background-solid, gradient-gauge, and lcd-gauge // modes are deprecated in favor of new cell subOptions -TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" | "pill" | "markdown" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions|Pill|Markdown") +TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" | "sparkline" | "data-links" | "custom" | "actions" | "pill" | "markdown" | "geo" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge|Sparkline|DataLinks|Custom|Actions|Pill|Markdown|Geo") // Display mode to the "Colored Background" display // mode for table cells. Either displays a solid color (basic mode) @@ -96,12 +96,20 @@ TableMarkdownCellOptions: { dynamicHeight?: bool } @cuetsy(kind="interface") +TableMarkdownCellOptions: { + type: TableCellDisplayMode & "markdown" +} @cuetsy(kind="interface") + +TableGeoCellOptions: { + type: TableCellDisplayMode & "geo" +} + // 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 @cuetsy(kind="type") +TableCellOptions: TableAutoCellOptions | TableSparklineCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TablePillCellOptions | TableDataLinksCellOptions | TableActionsCellOptions | TableJsonViewCellOptions | TableMarkdownCellOptions | TableGeoCellOptions @cuetsy(kind="type") // Field options for each field within a table (e.g 10, "The String", 64.20, etc.) // Generally defines alignment, filtering capabilties, display options, etc. diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx index 087efa47ecc..c8c2465a07a 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ActionsCell.tsx @@ -1,29 +1,16 @@ import { css } from '@emotion/css'; -import { useMemo } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; - -import { useStyles2 } from '../../../../themes/ThemeContext'; import { ActionButton } from '../../../Actions/ActionButton'; -import { ActionCellProps } from '../types'; +import { ActionCellProps, TableCellStyles } from '../types'; export const ActionsCell = ({ field, rowIdx, getActions }: ActionCellProps) => { - const styles = useStyles2(getStyles); + const actions = getActions(field, rowIdx); - const actions = useMemo(() => getActions(field, rowIdx), [getActions, field, rowIdx]); + if (actions.length === 0) { + return null; + } - return ( -
- {actions.map((action, i) => ( - - ))} -
- ); + return actions.map((action, i) => ); }; -const getStyles = (theme: GrafanaTheme2) => ({ - buttonsGap: css({ - display: 'flex', - gap: 6, - }), -}); +export const getStyles: TableCellStyles = (theme) => css({ gap: theme.spacing(0.75) }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx index 43de0b5bb1e..e169c756a94 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/AutoCell.tsx @@ -1,9 +1,11 @@ +import { css } from '@emotion/css'; + import { formattedValueToString } from '@grafana/data'; import { MaybeWrapWithLink } from '../MaybeWrapWithLink'; -import { AutoCellProps } from '../types'; +import { AutoCellProps, TableCellStyles } from '../types'; -export default function AutoCell({ value, field, rowIdx }: AutoCellProps) { +export function AutoCell({ value, field, rowIdx }: AutoCellProps) { const displayValue = field.display!(value); const formattedValue = formattedValueToString(displayValue); return ( @@ -12,3 +14,24 @@ export default function AutoCell({ value, field, rowIdx }: AutoCellProps) { ); } + +export const getStyles: TableCellStyles = (_theme, { textWrap, shouldOverflow }) => + css({ + ...(textWrap && { whiteSpace: 'pre-line' }), + ...(shouldOverflow && { + '&:hover, &[aria-selected=true]': { + whiteSpace: 'pre-line', + }, + }), + }); + +export const getJsonCellStyles: TableCellStyles = (_theme, { textWrap, shouldOverflow }) => + css({ + fontFamily: 'monospace', + ...(textWrap && { whiteSpace: 'pre' }), + ...(shouldOverflow && { + '&:hover, &[aria-selected=true]': { + whiteSpace: 'pre', + }, + }), + }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx index 3f762218553..700dbf2e33e 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/BarGaugeCell.tsx @@ -3,8 +3,9 @@ import { BarGaugeDisplayMode, BarGaugeValueMode, TableCellDisplayMode } from '@g import { BarGauge } from '../../../BarGauge/BarGauge'; import { MaybeWrapWithLink } from '../MaybeWrapWithLink'; +import { TABLE } from '../constants'; import { BarGaugeCellProps } from '../types'; -import { extractPixelValue, getCellOptions, getAlignmentFactor } from '../utils'; +import { getCellOptions, getAlignmentFactor } from '../utils'; const defaultScale: ThresholdsConfig = { mode: ThresholdsMode.Absolute, @@ -23,7 +24,7 @@ const defaultScale: ThresholdsConfig = { export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: BarGaugeCellProps) => { const displayValue = field.display!(value); const cellOptions = getCellOptions(field); - const heightOffset = extractPixelValue(theme.spacing(1)); + const heightOffset = TABLE.CELL_PADDING * 2; let config = getFieldConfigWithMinMax(field, false); if (!config.thresholds) { @@ -45,12 +46,14 @@ export const BarGaugeCell = ({ value, field, theme, height, width, rowIdx }: Bar } const alignmentFactors = getAlignmentFactor(field, displayValue, rowIdx!); + // clamp the height of the gauge so it isn't stretched for large rows + const renderedHeight = Math.min(height - heightOffset, TABLE.MAX_CELL_HEIGHT); return ( { const links = getCellLinks(field, rowIdx); @@ -14,3 +16,26 @@ export const DataLinksCell = ({ field, rowIdx }: DataLinksCellProps) => { )); }; + +export const getStyles: TableCellStyles = (theme, { textWrap, textAlign }) => + css({ + ...(textWrap && { + flexDirection: 'column', + justifyContent: 'center', + alignItems: getJustifyContent(textAlign), + }), + '> a': { + flexWrap: 'nowrap', + ...(!textWrap && { + paddingInline: theme.spacing(0.5), + borderRight: `2px solid ${theme.colors.border.medium}`, + '&:first-child': { + paddingInlineStart: 0, + }, + '&:last-child': { + paddingInlineEnd: 0, + borderRight: 'none', + }, + }), + }, + }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/GeoCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/GeoCell.tsx index 33d1530f8a9..94c5498cece 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/GeoCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/GeoCell.tsx @@ -2,13 +2,10 @@ import { css } from '@emotion/css'; import WKT from 'ol/format/WKT'; import { Geometry } from 'ol/geom'; -import { useStyles2 } from '../../../../themes/ThemeContext'; -import { GeoCellProps } from '../types'; +import { GeoCellProps, TableCellStyles } from '../types'; -export function GeoCell({ value, justifyContent, height }: GeoCellProps) { - const styles = useStyles2(getStyles); - - let disp = ''; +export function GeoCell({ value }: GeoCellProps) { + let disp = null; if (value instanceof Geometry) { disp = new WKT().writeGeometry(value, { @@ -19,25 +16,13 @@ export function GeoCell({ value, justifyContent, height }: GeoCellProps) { disp = `${value}`; } - return ( -
-
- {disp} -
-
- ); + return disp; } -const getStyles = () => ({ - cell: css({ - height: '100%', - display: 'flex', - alignItems: 'center', - padding: '0 8px', - }), - cellText: css({ +export const getStyles: TableCellStyles = () => + css({ + fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', - }), -}); + }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx index a9fd4420ff3..c3b085cbb5b 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/ImageCell.tsx @@ -1,36 +1,28 @@ import { css } from '@emotion/css'; -import { Property } from 'csstype'; -import { GrafanaTheme2 } from '@grafana/data'; - -import { useStyles2 } from '../../../../themes/ThemeContext'; import { TableCellDisplayMode } from '../../types'; import { MaybeWrapWithLink } from '../MaybeWrapWithLink'; -import { ImageCellProps } from '../types'; - -export const ImageCell = ({ cellOptions, field, height, justifyContent, value, rowIdx }: ImageCellProps) => { - const styles = useStyles2(getStyles, height, justifyContent); +import { ImageCellProps, TableCellStyles } from '../types'; +export const ImageCell = ({ cellOptions, field, value, rowIdx }: ImageCellProps) => { const { text } = field.display!(value); const { alt, title } = cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { alt: undefined, title: undefined }; return ( -
- - {alt} - -
+ + {alt} + ); }; -const getStyles = (_theme: GrafanaTheme2, height: number, justifyContent: Property.JustifyContent) => ({ - image: css({ - height, - width: 'auto', - }), - imageContainer: css({ - display: 'flex', - justifyContent, - }), -}); +export const getStyles: TableCellStyles = () => + css({ + 'a, img': { + width: '100%', + height: '100%', + }, + img: { + objectFit: 'contain', + }, + }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx index 7d4922c2434..4bd252ff9d3 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/MarkdownCell.tsx @@ -1,7 +1,9 @@ +import { css } from '@emotion/css'; + import { renderMarkdown } from '@grafana/data'; import { MaybeWrapWithLink } from '../MaybeWrapWithLink'; -import { MarkdownCellProps } from '../types'; +import { MarkdownCellProps, TableCellStyles } from '../types'; export function MarkdownCell({ field, rowIdx, disableSanitizeHtml }: MarkdownCellProps) { return ( @@ -15,3 +17,29 @@ export function MarkdownCell({ field, rowIdx, disableSanitizeHtml }: MarkdownCel
); } + +export const getStyles: TableCellStyles = (theme) => + css({ + '&, &:hover, &[aria-selected=true]': { + whiteSpace: 'normal', + }, + + '.markdown-container': { + width: '100%', + }, + + '& ol, & ul': { + paddingLeft: theme.spacing(1.5), + }, + '& p': { + whiteSpace: 'pre-line', + }, + '& a': { + color: theme.colors.primary.text, + }, + // for elements like `p`, `h*`, etc. which have an inherent margin, + // we want to remove the bottom margin for the last one in the container. + '& > .markdown-container > *:last-child': { + marginBottom: 0, + }, + }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx index 35b759aa224..2b85560c792 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/PillCell.tsx @@ -1,3 +1,4 @@ +import { css } from '@emotion/css'; import { useMemo } from 'react'; import { @@ -11,7 +12,7 @@ import { } from '@grafana/data'; import { FieldColorModeId } from '@grafana/schema'; -import { PillCellProps, TableCellValue } from '../types'; +import { PillCellProps, TableCellStyles, TableCellValue } from '../types'; export function PillCell({ rowIdx, field, theme }: PillCellProps) { const value = field.values[rowIdx]; @@ -102,3 +103,25 @@ function getPillColor(value: string, field: Field, theme: GrafanaTheme2): string return getColorByStringHash(colors, value); } + +export const getStyles: TableCellStyles = (theme, { textWrap, shouldOverflow }) => + css({ + display: 'inline-flex', + gap: theme.spacing(0.5), + flexWrap: textWrap ? 'wrap' : 'nowrap', + + ...(shouldOverflow && { + '&:hover, &[aria-selected=true]': { + flexWrap: 'wrap', + }, + }), + + '> span': { + display: 'flex', + padding: theme.spacing(0.25, 0.75), + borderRadius: theme.shape.radius.default, + fontSize: theme.typography.bodySmall.fontSize, + lineHeight: theme.typography.bodySmall.lineHeight, + whiteSpace: 'nowrap', + }, + }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx index 4979981995b..08f8b8b6cd1 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx @@ -1,5 +1,4 @@ import { css } from '@emotion/css'; -import { Property } from 'csstype'; import * as React from 'react'; import { @@ -10,7 +9,6 @@ import { isDataFrame, Field, isDataFrameWithValue, - GrafanaTheme2, } from '@grafana/data'; import { t } from '@grafana/i18n'; import { @@ -24,11 +22,10 @@ import { VisibilityMode, } from '@grafana/schema'; -import { useStyles2 } from '../../../../themes/ThemeContext'; import { measureText } from '../../../../utils/measureText'; import { FormattedValueDisplay } from '../../../FormattedValueDisplay/FormattedValueDisplay'; import { Sparkline } from '../../../Sparkline/Sparkline'; -import { SparklineCellProps } from '../types'; +import { SparklineCellProps, TableCellStyles } from '../types'; import { getAlignmentFactor, getCellOptions } from '../utils'; export const defaultSparklineCellConfig: TableSparklineCellOptions = { @@ -45,8 +42,7 @@ export const defaultSparklineCellConfig: TableSparklineCellOptions = { }; export const SparklineCell = (props: SparklineCellProps) => { - const { field, value, theme, timeRange, rowIdx, justifyContent, width } = props; - const styles = useStyles2(getStyles, justifyContent); + const { field, value, theme, timeRange, rowIdx, width } = props; const sparkline = getSparkline(value, field); if (!sparkline) { @@ -95,26 +91,14 @@ export const SparklineCell = (props: SparklineCellProps) => { measureText(`${alignmentFactor.prefix ?? ''}${alignmentFactor.text}${alignmentFactor.suffix ?? ''}`, 16).width + theme.spacing.gridSize; - valueElement = ( - - ); + valueElement = ; } - // @TODO update width, height return ( -
+ <> {valueElement} -
+ ); }; @@ -153,14 +137,10 @@ function getTableSparklineCellOptions(field: Field): TableSparklineCellOptions { throw new Error(`Expected options type ${TableCellDisplayMode.Sparkline} but got ${options.type}`); } -const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent | undefined) => ({ - cellContainer: css({ - display: 'flex', +export const getStyles: TableCellStyles = (theme, { textAlign }) => + css({ width: '100%', - alignItems: 'center', - justifyContent, - }), - valueContainer: css({ - div: { width: 'inherit' }, - }), -}); + gap: theme.spacing(1), + justifyContent: 'space-between', + ...(textAlign === 'right' && { flexDirection: 'row-reverse' }), + }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.test.tsx index 44221577315..3c4e5421c4c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.test.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.test.tsx @@ -89,7 +89,9 @@ describe('TableNG Cells renderers', () => { cellOptions, cellInspect: false, showFilters: false, - justifyContent: 'flex-start', + getActions: jest.fn(() => [ + { title: 'Action', onClick: jest.fn(() => {}), confirmation: jest.fn(), style: {} }, + ]), }) ); @@ -110,7 +112,6 @@ describe('TableNG Cells renderers', () => { cellOptions, cellInspect: false, showFilters: false, - justifyContent: 'flex-start', }) ); }, iterations); @@ -123,8 +124,8 @@ describe('TableNG Cells renderers', () => { { type: TableCellDisplayMode.JSONView, fieldType: FieldType.string }, { type: TableCellDisplayMode.Image, fieldType: FieldType.string }, { type: TableCellDisplayMode.DataLinks, fieldType: FieldType.string }, - { type: TableCellDisplayMode.Actions, fieldType: FieldType.string }, { type: TableCellDisplayMode.ColorText, fieldType: FieldType.string }, + { type: TableCellDisplayMode.Actions, fieldType: FieldType.string }, { type: TableCellDisplayMode.ColorBackground, fieldType: FieldType.string }, { type: TableCellDisplayMode.Auto, fieldType: FieldType.string }, ] as const)('should render $type cell into the document', ({ type, fieldType }) => { diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx index c406d7ffc3f..74648953454 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx @@ -1,19 +1,20 @@ +import { clsx } from 'clsx'; import { ReactNode } from 'react'; -import { Field, FieldType, isDataFrame, isTimeSeriesFrame } from '@grafana/data'; +import { Field, FieldType, GrafanaTheme2, isDataFrame, isTimeSeriesFrame } from '@grafana/data'; import { TableCellDisplayMode, TableCellOptions, TableCustomCellOptions } from '../../types'; -import { TableCellRendererProps } from '../types'; +import { TableCellRendererProps, TableCellStyleOptions, TableCellStyles } from '../types'; -import { ActionsCell } from './ActionsCell'; -import AutoCell from './AutoCell'; +import { ActionsCell, getStyles as getActionsCellStyles } from './ActionsCell'; +import { AutoCell, getStyles as getAutoCellStyles, getJsonCellStyles } from './AutoCell'; import { BarGaugeCell } from './BarGaugeCell'; -import { DataLinksCell } from './DataLinksCell'; -import { GeoCell } from './GeoCell'; -import { ImageCell } from './ImageCell'; -import { MarkdownCell } from './MarkdownCell'; -import { PillCell } from './PillCell'; -import { SparklineCell } from './SparklineCell'; +import { DataLinksCell, getStyles as getDataLinksStyles } from './DataLinksCell'; +import { GeoCell, getStyles as getGeoCellStyles } from './GeoCell'; +import { ImageCell, getStyles as getImageStyles } from './ImageCell'; +import { MarkdownCell, getStyles as getMarkdownCellStyles } from './MarkdownCell'; +import { PillCell, getStyles as getPillStyles } from './PillCell'; +import { SparklineCell, getStyles as getSparklineCellStyles } from './SparklineCell'; export type TableCellRenderer = (props: TableCellRendererProps) => ReactNode; @@ -36,7 +37,6 @@ const SPARKLINE_RENDERER: TableCellRenderer = (props) => ( ( /> ); -const GEO_RENDERER: TableCellRenderer = (props) => ( - -); +const GEO_RENDERER: TableCellRenderer = (props) => ; const IMAGE_RENDERER: TableCellRenderer = (props) => ( - + ); const DATA_LINKS_RENDERER: TableCellRenderer = (props) => ; @@ -65,12 +56,23 @@ const ACTIONS_RENDERER: TableCellRenderer = ({ field, rowIdx, getActions = () => ); +const MARKDOWN_RENDERER: TableCellRenderer = (props) => ( + +); + const PILL_RENDERER: TableCellRenderer = (props) => ; function isCustomCellOptions(options: TableCellOptions): options is TableCustomCellOptions { return options.type === TableCellDisplayMode.Custom; } +function mixinAutoCellStyles(fn: TableCellStyles): TableCellStyles { + return (theme, options) => { + const styles = fn(theme, options); + return clsx(styles, getAutoCellStyles(theme, options)); + }; +} + const CUSTOM_RENDERER: TableCellRenderer = (props) => { if (!isCustomCellOptions(props.cellOptions) || !props.cellOptions.cellComponent) { return null; // nonsensical case, but better to typeguard it than throw. @@ -79,23 +81,57 @@ const CUSTOM_RENDERER: TableCellRenderer = (props) => { return ; }; -const MARKDOWN_RENDERER: TableCellRenderer = (props) => ( - -); - -const CELL_RENDERERS: Record = { - [TableCellDisplayMode.Sparkline]: SPARKLINE_RENDERER, - [TableCellDisplayMode.Gauge]: GAUGE_RENDERER, - [TableCellDisplayMode.JSONView]: AUTO_RENDERER, - [TableCellDisplayMode.Image]: IMAGE_RENDERER, - [TableCellDisplayMode.DataLinks]: DATA_LINKS_RENDERER, - [TableCellDisplayMode.Actions]: ACTIONS_RENDERER, - [TableCellDisplayMode.Custom]: CUSTOM_RENDERER, - [TableCellDisplayMode.ColorText]: AUTO_RENDERER, - [TableCellDisplayMode.ColorBackground]: AUTO_RENDERER, - [TableCellDisplayMode.Auto]: AUTO_RENDERER, - [TableCellDisplayMode.Markdown]: MARKDOWN_RENDERER, - [TableCellDisplayMode.Pill]: PILL_RENDERER, +const CELL_RENDERERS: Record = { + [TableCellDisplayMode.Actions]: { + renderer: ACTIONS_RENDERER, + getStyles: getActionsCellStyles, + }, + [TableCellDisplayMode.Auto]: { + renderer: AUTO_RENDERER, + getStyles: getAutoCellStyles, + }, + [TableCellDisplayMode.ColorBackground]: { + renderer: AUTO_RENDERER, + getStyles: getAutoCellStyles, + }, + [TableCellDisplayMode.ColorText]: { + renderer: AUTO_RENDERER, + getStyles: getAutoCellStyles, + }, + [TableCellDisplayMode.Custom]: { + renderer: CUSTOM_RENDERER, + }, + [TableCellDisplayMode.DataLinks]: { + renderer: DATA_LINKS_RENDERER, + getStyles: getDataLinksStyles, + }, + [TableCellDisplayMode.Gauge]: { + renderer: GAUGE_RENDERER, + }, + [TableCellDisplayMode.Geo]: { + renderer: GEO_RENDERER, + getStyles: getGeoCellStyles, + }, + [TableCellDisplayMode.Image]: { + renderer: IMAGE_RENDERER, + getStyles: getImageStyles, + }, + [TableCellDisplayMode.JSONView]: { + renderer: AUTO_RENDERER, + getStyles: mixinAutoCellStyles(getJsonCellStyles), + }, + [TableCellDisplayMode.Pill]: { + renderer: PILL_RENDERER, + getStyles: getPillStyles, + }, + [TableCellDisplayMode.Sparkline]: { + renderer: SPARKLINE_RENDERER, + getStyles: getSparklineCellStyles, + }, + [TableCellDisplayMode.Markdown]: { + renderer: MARKDOWN_RENDERER, + getStyles: getMarkdownCellStyles, + }, }; // TODO: come up with a more elegant way to handle this. @@ -108,26 +144,53 @@ const STRING_ONLY_RENDERERS = new Set([ export function getCellRenderer(field: Field, cellOptions: TableCellOptions): TableCellRenderer { const cellType = cellOptions?.type ?? TableCellDisplayMode.Auto; if (cellType === TableCellDisplayMode.Auto) { - return getAutoRendererResult(field); + return CELL_RENDERERS[getAutoRendererDisplayMode(field)].renderer; } if (STRING_ONLY_RENDERERS.has(cellType) && field.type !== FieldType.string) { return AUTO_RENDERER; } - return CELL_RENDERERS[cellType] ?? AUTO_RENDERER; + // cautious fallback to Auto renderer in case some garbage cell type has been provided. + return CELL_RENDERERS[cellType]?.renderer ?? AUTO_RENDERER; } /** @internal */ -export function getAutoRendererResult(field: Field): TableCellRenderer { +export function getCellSpecificStyles( + cellType: TableCellOptions['type'], + field: Field, + theme: GrafanaTheme2, + options: TableCellStyleOptions +): string | undefined { + if (cellType === TableCellDisplayMode.Auto) { + return getAutoRendererStyles(theme, options, field); + } + return CELL_RENDERERS[cellType]?.getStyles?.(theme, options); +} + +/** @internal */ +export function getAutoRendererStyles( + theme: GrafanaTheme2, + options: TableCellStyleOptions, + field: Field +): string | undefined { + const impliedDisplayMode = getAutoRendererDisplayMode(field); + if (impliedDisplayMode !== TableCellDisplayMode.Auto) { + return CELL_RENDERERS[impliedDisplayMode]?.getStyles?.(theme, options); + } + return getAutoCellStyles(theme, options); +} + +/** @internal */ +export function getAutoRendererDisplayMode(field: Field): TableCellOptions['type'] { if (field.type === FieldType.geo) { - return GEO_RENDERER; + return TableCellDisplayMode.Geo; } if (field.type === FieldType.frame) { const firstValue = field.values[0]; if (isDataFrame(firstValue) && isTimeSeriesFrame(firstValue)) { - return SPARKLINE_RENDERER; + return TableCellDisplayMode.Sparkline; } } - return AUTO_RENDERER; + return TableCellDisplayMode.Auto; } diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index f8bf1c16491..4adbf7541c9 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -1,7 +1,6 @@ import 'react-data-grid/lib/styles.css'; -import { css } from '@emotion/css'; + import { clsx } from 'clsx'; -import { Property } from 'csstype'; import { CSSProperties, Key, ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Cell, @@ -22,7 +21,6 @@ import { Field, FieldType, getDisplayProcessor, - GrafanaTheme2, ReducerID, } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; @@ -38,10 +36,10 @@ import { TableCellInspector, TableCellInspectorMode } from '../TableCellInspecto import { TableCellDisplayMode } from '../types'; import { DataLinksActionsTooltipState } from '../utils'; -import { HeaderCell } from './Cells/HeaderCell'; -import { RowExpander } from './Cells/RowExpander'; -import { TableCellActions } from './Cells/TableCellActions'; -import { getCellRenderer } from './Cells/renderers'; +import { getCellRenderer, getCellSpecificStyles } from './Cells/renderers'; +import { HeaderCell } from './components/HeaderCell'; +import { RowExpander } from './components/RowExpander'; +import { TableCellActions } from './components/TableCellActions'; import { COLUMN, TABLE } from './constants'; import { useColumnResize, @@ -53,7 +51,8 @@ import { useScrollbarWidth, useSortedRows, } from './hooks'; -import { TableNGProps, TableRow, TableSummaryRow, TableColumn, ContextMenuProps } from './types'; +import { getDefaultCellStyles, getFooterStyles, getGridStyles, getHeaderCellStyles, getLinkStyles } from './styles'; +import { TableNGProps, TableRow, TableSummaryRow, TableColumn, ContextMenuProps, TableCellStyleOptions } from './types'; import { applySort, computeColWidths, @@ -74,7 +73,6 @@ import { isCellInspectEnabled, shouldTextOverflow, shouldTextWrap, - TextAlign, withDataLinksActionsTooltip, } from './utils'; @@ -364,37 +362,19 @@ export function TableNG(props: TableNGProps) { : undefined; const shouldOverflow = rowHeight !== 'auto' && shouldTextOverflow(field); - const shouldWrap = rowHeight === 'auto' || shouldTextWrap(field); + const textWrap = rowHeight === 'auto' || shouldTextWrap(field); const withTooltip = withDataLinksActionsTooltip(field, cellType); const canBeColorized = - cellType === TableCellDisplayMode.ColorBackground || cellType === TableCellDisplayMode.ColorText; - const isMonospace = cellType === TableCellDisplayMode.JSONView; + cellType === TableCellDisplayMode.ColorBackground || + cellType === TableCellDisplayMode.ColorText || + Boolean(applyToRowBgFn); + const cellStyleOptions: TableCellStyleOptions = { textAlign, textWrap, shouldOverflow }; result.colsWithTooltip[displayName] = withTooltip; - // get static cell class based on col props - - let cellClass = ''; - - switch (cellType) { - case TableCellDisplayMode.Auto: - case TableCellDisplayMode.ColorBackground: - case TableCellDisplayMode.ColorText: - case TableCellDisplayMode.DataLinks: - case TableCellDisplayMode.JSONView: - case TableCellDisplayMode.Pill: - case TableCellDisplayMode.Markdown: - cellClass = getCellStyles( - theme, - cellType, - textAlign, - shouldWrap, - shouldOverflow, - canBeColorized, - isMonospace - ); - break; - } + const defaultCellStyles = getDefaultCellStyles(theme, cellStyleOptions); + const cellSpecificStyles = getCellSpecificStyles(cellType, field, theme, cellStyleOptions); + const linkStyles = getLinkStyles(theme, canBeColorized); // TODO: in future extend this to ensure a non-classic color scheme is set with AutoCell @@ -433,7 +413,14 @@ export function TableNG(props: TableNGProps) { }; } - return ; // TODO: remove expensive concat + return ( + + ); }; result.cellRootRenderers[displayName] = renderCellRoot; @@ -456,7 +443,6 @@ export function TableNG(props: TableNGProps) { frame, field, height, - justifyContent, rowIdx, theme, value, @@ -812,264 +798,3 @@ const renderRowFactory = return ; }; - -const getGridStyles = ( - theme: GrafanaTheme2, - { enablePagination, transparent }: { enablePagination?: boolean; transparent?: boolean } -) => ({ - grid: css({ - '--rdg-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, - '--rdg-header-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, - '--rdg-border-color': theme.colors.border.weak, - '--rdg-color': theme.colors.text.primary, - '--rdg-summary-border-color': theme.colors.border.weak, - '--rdg-summary-border-width': '1px', - - // note: this cannot have any transparency since default cells that - // overlay/overflow on hover inherit this background and need to occlude cells below - '--rdg-row-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, - '--rdg-row-hover-background-color': transparent - ? theme.colors.background.primary - : theme.colors.background.secondary, - - // TODO: magic 32px number is unfortunate. it would be better to have the content - // flow using flexbox rather than hard-coding this size via a calc - blockSize: enablePagination ? 'calc(100% - 32px)' : '100%', - scrollbarWidth: 'thin', - scrollbarColor: theme.isDark ? '#fff5 #fff1' : '#0005 #0001', - - border: 'none', - - '.rdg-cell': { - padding: TABLE.CELL_PADDING, - '&:last-child': { - borderInlineEnd: 'none', - }, - }, - - // add a box shadow on hover and selection for all body cells - '& > :not(.rdg-summary-row, .rdg-header-row) > .rdg-cell': { - '&:hover, &[aria-selected=true]': { - boxShadow: theme.shadows.z2, - }, - // selected cells should appear below hovered cells. - '&:hover': { - zIndex: theme.zIndex.tooltip - 2, - }, - '&[aria-selected=true]': { - zIndex: theme.zIndex.tooltip - 3, - }, - }, - - '.rdg-header-row, .rdg-summary-row': { - '.rdg-cell': { - zIndex: theme.zIndex.tooltip - 1, - }, - }, - }), - gridNested: css({ - height: '100%', - width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING * 2 - 1}px)`, - overflow: 'visible', - marginLeft: COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING - 1, - marginBlock: TABLE.CELL_PADDING, - }), - cellNested: css({ - '&[aria-selected=true]': { - outline: 'none', - }, - }), - noDataNested: css({ - height: TABLE.NESTED_NO_DATA_HEIGHT, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: theme.colors.text.secondary, - fontSize: theme.typography.h4.fontSize, - }), - cellActions: css({ - display: 'none', - position: 'absolute', - top: 0, - margin: 'auto', - height: '100%', - color: theme.colors.text.primary, - background: theme.isDark ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)', - padding: theme.spacing.x0_5, - paddingInlineStart: theme.spacing.x1, - }), - cellActionsEnd: css({ - left: 0, - }), - cellActionsStart: css({ - right: 0, - }), - headerRow: css({ - paddingBlockStart: 0, - fontWeight: 'normal', - '& .rdg-cell': { - height: '100%', - alignItems: 'flex-end', - }, - }), - displayNone: css({ - display: 'none', - }), - paginationContainer: css({ - alignItems: 'center', - display: 'flex', - justifyContent: 'center', - marginTop: '8px', - width: '100%', - }), - paginationSummary: css({ - color: theme.colors.text.secondary, - fontSize: theme.typography.bodySmall.fontSize, - display: 'flex', - justifyContent: 'flex-end', - padding: theme.spacing(0, 1, 0, 2), - }), - menuItem: css({ - maxWidth: '200px', - }), -}); - -const getFooterStyles = (justifyContent: Property.JustifyContent) => ({ - footerCellCountRows: css({ - display: 'flex', - justifyContent: 'space-between', - }), - footerCell: css({ - display: 'flex', - justifyContent: justifyContent || 'space-between', - }), -}); - -const getHeaderCellStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => - css({ - display: 'flex', - gap: theme.spacing(0.5), - zIndex: theme.zIndex.tooltip - 1, - paddingInline: TABLE.CELL_PADDING, - paddingBlockEnd: TABLE.CELL_PADDING, - justifyContent, - '&:last-child': { - borderInlineEnd: 'none', - }, - }); - -const getCellStyles = ( - theme: GrafanaTheme2, - cellType: TableCellDisplayMode, - textAlign: TextAlign, - shouldWrap: boolean, - shouldOverflow: boolean, - isColorized: boolean, - isMonospace: boolean -) => { - const whiteSpace: CSSProperties['whiteSpace'] = (() => { - if (isMonospace) { - return 'pre'; - } - if (cellType === TableCellDisplayMode.Markdown) { - return 'normal'; - } - return 'pre-line'; - })(); - - return css({ - display: 'flex', - alignItems: 'center', - textAlign, - justifyContent: getJustifyContent(textAlign), - - ...(isColorized && { backgroundClip: 'padding-box !important' }), - ...(shouldOverflow && { minHeight: '100%' }), - ...(shouldWrap && { whiteSpace }), - ...(isMonospace && { fontFamily: 'monospace' }), - - '&:hover, &[aria-selected=true]': { - '.table-cell-actions': { - display: 'flex', - }, - ...(shouldOverflow && { - zIndex: theme.zIndex.tooltip - 2, - whiteSpace, - height: 'fit-content', - minWidth: 'fit-content', - ...(cellType === TableCellDisplayMode.Pill && { - flexWrap: 'wrap', - }), - }), - }, - - a: { - cursor: 'pointer', - ...(isColorized - ? { - color: 'inherit', - textDecoration: 'underline', - } - : { - color: theme.colors.text.link, - textDecoration: 'none', - '&:hover': { - textDecoration: 'underline', - }, - }), - }, - - ...(cellType === TableCellDisplayMode.DataLinks && { - ...(shouldWrap && { - flexDirection: 'column', - justifyContent: 'center', - alignItems: getJustifyContent(textAlign), - }), - '> a': { - flexWrap: 'nowrap', - ...(!shouldWrap && { - paddingInline: theme.spacing(0.5), - borderRight: `2px solid ${theme.colors.border.medium}`, - '&:first-child': { - paddingInlineStart: 0, - }, - '&:last-child': { - paddingInlineEnd: 0, - borderRight: 'none', - }, - }), - }, - }), - - ...(cellType === TableCellDisplayMode.Pill && { - display: 'inline-flex', - gap: theme.spacing(0.5), - flexWrap: shouldWrap ? 'wrap' : 'nowrap', - '> span': { - display: 'flex', - padding: theme.spacing(0.25, 0.75), - borderRadius: theme.shape.radius.default, - fontSize: theme.typography.bodySmall.fontSize, - lineHeight: theme.typography.bodySmall.lineHeight, - whiteSpace: 'nowrap', - }, - }), - - ...(cellType === TableCellDisplayMode.Markdown && { - '& ol, & ul': { - paddingLeft: theme.spacing(1.5), - }, - '& p': { - whiteSpace: 'pre-line', - }, - '& a': { - color: theme.colors.primary.text, - }, - // for elements like `p`, `h*`, etc. which have an inherent margin, - // we want to remove the bottom margin for the last one in the container. - '& > .markdown-container > *:last-child': { - marginBottom: 0, - }, - }), - }); -}; diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/FooterCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/FooterCell.tsx similarity index 100% rename from packages/grafana-ui/src/components/Table/TableNG/Cells/FooterCell.tsx rename to packages/grafana-ui/src/components/Table/TableNG/components/FooterCell.tsx diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/HeaderCell.tsx similarity index 96% rename from packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx rename to packages/grafana-ui/src/components/Table/TableNG/components/HeaderCell.tsx index a62cbeb6beb..8ccc73a98b9 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/HeaderCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/components/HeaderCell.tsx @@ -89,7 +89,10 @@ const getStyles = (theme: GrafanaTheme2, headerTextWrap?: boolean) => ({ whiteSpace: headerTextWrap ? 'pre-line' : 'nowrap', '&:hover': { textDecoration: 'underline', - color: theme.colors.text.link, + }, + '&::selection': { + backgroundColor: 'var(--rdg-background-color)', + color: theme.colors.text.secondary, }, }), headerCellIcon: css({ diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/RowExpander.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/RowExpander.tsx similarity index 100% rename from packages/grafana-ui/src/components/Table/TableNG/Cells/RowExpander.tsx rename to packages/grafana-ui/src/components/Table/TableNG/components/RowExpander.tsx diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx b/packages/grafana-ui/src/components/Table/TableNG/components/TableCellActions.tsx similarity index 100% rename from packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellActions.tsx rename to packages/grafana-ui/src/components/Table/TableNG/components/TableCellActions.tsx diff --git a/packages/grafana-ui/src/components/Table/TableNG/constants.ts b/packages/grafana-ui/src/components/Table/TableNG/constants.ts index 7638dd98804..e88f7181591 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/constants.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/constants.ts @@ -13,7 +13,6 @@ export const TABLE = { PAGINATION_LIMIT: 750, SCROLL_BAR_WIDTH: 8, SCROLL_BAR_MARGIN: 2, - FONT_SIZE: 14, LINE_HEIGHT: 22, HEADER_HEIGHT: 28, NESTED_NO_DATA_HEIGHT: 60, diff --git a/packages/grafana-ui/src/components/Table/TableNG/styles.ts b/packages/grafana-ui/src/components/Table/TableNG/styles.ts new file mode 100644 index 00000000000..9fed490c224 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableNG/styles.ts @@ -0,0 +1,158 @@ +import { css } from '@emotion/css'; +import { Property } from 'csstype'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { COLUMN, TABLE } from './constants'; +import { TableCellStyles } from './types'; +import { getJustifyContent } from './utils'; + +export const getGridStyles = ( + theme: GrafanaTheme2, + { enablePagination, transparent }: { enablePagination?: boolean; transparent?: boolean } +) => ({ + grid: css({ + '--rdg-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, + '--rdg-header-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, + '--rdg-border-color': theme.colors.border.weak, + '--rdg-color': theme.colors.text.primary, + '--rdg-summary-border-color': theme.colors.border.weak, + '--rdg-summary-border-width': '1px', + + // note: this cannot have any transparency since default cells that + // overlay/overflow on hover inherit this background and need to occlude cells below + '--rdg-row-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, + '--rdg-row-hover-background-color': transparent + ? theme.colors.background.primary + : theme.colors.background.secondary, + + // TODO: magic 32px number is unfortunate. it would be better to have the content + // flow using flexbox rather than hard-coding this size via a calc + blockSize: enablePagination ? 'calc(100% - 32px)' : '100%', + scrollbarWidth: 'thin', + scrollbarColor: theme.isDark ? '#fff5 #fff1' : '#0005 #0001', + + border: 'none', + + '.rdg-cell': { + padding: TABLE.CELL_PADDING, + + '&:last-child': { + borderInlineEnd: 'none', + }, + }, + + // add a box shadow on hover and selection for all body cells + '& > :not(.rdg-summary-row, .rdg-header-row) > .rdg-cell': { + '&:hover, &[aria-selected=true]': { boxShadow: theme.shadows.z2 }, + // selected cells should appear below hovered cells. + '&:hover': { zIndex: theme.zIndex.tooltip - 2 }, + '&[aria-selected=true]': { zIndex: theme.zIndex.tooltip - 3 }, + }, + + '.rdg-header-row, .rdg-summary-row': { + '.rdg-cell': { zIndex: theme.zIndex.tooltip - 1 }, + }, + }), + gridNested: css({ + height: '100%', + width: `calc(100% - ${COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING * 2 - 1}px)`, + overflow: 'visible', + marginLeft: COLUMN.EXPANDER_WIDTH - TABLE.CELL_PADDING - 1, + marginBlock: TABLE.CELL_PADDING, + }), + cellNested: css({ '&[aria-selected=true]': { outline: 'none' } }), + noDataNested: css({ + height: TABLE.NESTED_NO_DATA_HEIGHT, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: theme.colors.text.secondary, + fontSize: theme.typography.h4.fontSize, + }), + cellActions: css({ + display: 'none', + position: 'absolute', + top: 0, + margin: 'auto', + height: '100%', + color: theme.colors.text.primary, + background: theme.isDark ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)', + padding: theme.spacing.x0_5, + paddingInlineStart: theme.spacing.x1, + }), + cellActionsEnd: css({ left: 0 }), + cellActionsStart: css({ right: 0 }), + headerRow: css({ + paddingBlockStart: 0, + fontWeight: 'normal', + '& .rdg-cell': { height: '100%', alignItems: 'flex-end' }, + }), + displayNone: css({ display: 'none' }), + paginationContainer: css({ + alignItems: 'center', + display: 'flex', + justifyContent: 'center', + marginTop: '8px', + width: '100%', + }), + paginationSummary: css({ + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + display: 'flex', + justifyContent: 'flex-end', + padding: theme.spacing(0, 1, 0, 2), + }), + menuItem: css({ maxWidth: '200px' }), +}); + +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', + gap: theme.spacing(0.5), + zIndex: theme.zIndex.tooltip - 1, + paddingInline: TABLE.CELL_PADDING, + paddingBlockEnd: TABLE.CELL_PADDING, + justifyContent, + '&:last-child': { borderInlineEnd: 'none' }, + }); + +export const getDefaultCellStyles: TableCellStyles = (theme, { textAlign, shouldOverflow }) => + css({ + display: 'flex', + alignItems: 'center', + textAlign, + backgroundClip: 'padding-box !important', // helps when cells have a bg color + justifyContent: getJustifyContent(textAlign), + ...(shouldOverflow && { minHeight: '100%' }), + '&:hover, &[aria-selected=true]': { + '.table-cell-actions': { display: 'flex' }, + ...(shouldOverflow && { + zIndex: theme.zIndex.tooltip - 2, + height: 'fit-content', + minWidth: 'fit-content', + }), + }, + }); + +export const getLinkStyles = (theme: GrafanaTheme2, canBeColorized: boolean) => + css({ + a: { + cursor: 'pointer', + ...(canBeColorized + ? { + color: 'inherit', + textDecoration: 'underline', + } + : { + color: theme.colors.text.link, + textDecoration: 'none', + '&:hover': { textDecoration: 'underline' }, + }), + }, + }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts index eaca19734ea..105e900189c 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/types.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -1,4 +1,3 @@ -import { Property } from 'csstype'; import { SyntheticEvent } from 'react'; import { Column } from 'react-data-grid'; @@ -19,6 +18,8 @@ import { TableCellHeight, TableFieldOptions } from '@grafana/schema'; import { TableCellInspectorMode } from '../TableCellInspector'; import { TableCellOptions } from '../types'; +import { TextAlign } from './utils'; + export const FILTER_FOR_OPERATOR = '='; export const FILTER_OUT_OPERATOR = '!='; @@ -161,7 +162,6 @@ export interface TableCellRendererProps { theme: GrafanaTheme2; cellInspect: boolean; showFilters: boolean; - justifyContent: Property.JustifyContent; getActions?: GetActionsFunctionLocal; disableSanitizeHtml?: boolean; } @@ -195,7 +195,6 @@ export interface RowExpanderNGProps { export interface SparklineCellProps { field: Field; - justifyContent: Property.JustifyContent; rowIdx: number; theme: GrafanaTheme2; timeRange?: TimeRange; @@ -215,8 +214,6 @@ export interface BarGaugeCellProps { export interface ImageCellProps { cellOptions: TableCellOptions; field: Field; - height: number; - justifyContent: Property.JustifyContent; value: TableCellValue; rowIdx: number; } @@ -228,7 +225,6 @@ export interface DataLinksCellProps { export interface GeoCellProps { value: TableCellValue; - justifyContent: Property.JustifyContent; height: number; } @@ -262,6 +258,14 @@ export interface PillCellProps { rowIdx: number; } +export interface TableCellStyleOptions { + textWrap: boolean; + textAlign: TextAlign; + shouldOverflow: boolean; +} + +export type TableCellStyles = (theme: GrafanaTheme2, options: TableCellStyleOptions) => string; + // Comparator for sorting table values export type Comparator = (a: TableCellValue, b: TableCellValue) => number;