Table.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. /* eslint-disable func-names, no-negated-condition */
  20. /* eslint-disable prefer-destructuring */
  21. /* eslint-disable react/sort-prop-types */
  22. import d3 from 'd3';
  23. import PropTypes from 'prop-types';
  24. import dt from 'datatables.net-bs/js/dataTables.bootstrap';
  25. import dompurify from 'dompurify';
  26. import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format';
  27. import { getTimeFormatter } from '@superset-ui/time-format';
  28. import fixTableHeight from './utils/fixTableHeight';
  29. import 'datatables.net-bs/css/dataTables.bootstrap.css';
  30. import './Table.css';
  31. if (window.$) {
  32. dt(window, window.$);
  33. }
  34. const $ = window.$ || dt.$;
  35. const propTypes = {
  36. // Each object is { field1: value1, field2: value2 }
  37. data: PropTypes.arrayOf(PropTypes.object),
  38. height: PropTypes.number,
  39. alignPositiveNegative: PropTypes.bool,
  40. colorPositiveNegative: PropTypes.bool,
  41. columns: PropTypes.arrayOf(PropTypes.shape({
  42. key: PropTypes.string,
  43. label: PropTypes.string,
  44. format: PropTypes.string
  45. })),
  46. filters: PropTypes.object,
  47. includeSearch: PropTypes.bool,
  48. metrics: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object])),
  49. onAddFilter: PropTypes.func,
  50. onRemoveFilter: PropTypes.func,
  51. orderDesc: PropTypes.bool,
  52. pageLength: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  53. percentMetrics: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object])),
  54. tableFilter: PropTypes.bool,
  55. tableTimestampFormat: PropTypes.string,
  56. timeseriesLimitMetric: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
  57. };
  58. const formatValue = getNumberFormatter(NumberFormats.INTEGER);
  59. const formatPercent = getNumberFormatter(NumberFormats.PERCENT_3_POINT);
  60. const NOOP = () => {};
  61. function TableVis(element, props) {
  62. const {
  63. data,
  64. height,
  65. alignPositiveNegative = false,
  66. colorPositiveNegative = false,
  67. columns,
  68. filters = {},
  69. includeSearch = false,
  70. metrics: rawMetrics,
  71. onAddFilter = NOOP,
  72. onRemoveFilter = NOOP,
  73. orderDesc,
  74. pageLength,
  75. percentMetrics,
  76. tableFilter,
  77. tableTimestampFormat,
  78. timeseriesLimitMetric
  79. } = props;
  80. const $container = $(element);
  81. $container.addClass('superset-legacy-chart-table');
  82. const metrics = (rawMetrics || []).map(m => m.label || m) // Add percent metrics
  83. .concat((percentMetrics || []).map(m => "%" + m)) // Removing metrics (aggregates) that are strings
  84. .filter(m => typeof data[0][m] === 'number');
  85. function col(c) {
  86. const arr = [];
  87. data.forEach(row => {
  88. arr.push(row[c]);
  89. });
  90. return arr;
  91. }
  92. const maxes = {};
  93. const mins = {};
  94. metrics.forEach(metric => {
  95. if (alignPositiveNegative) {
  96. maxes[metric] = d3.max(col(metric).map(Math.abs));
  97. } else {
  98. maxes[metric] = d3.max(col(metric));
  99. mins[metric] = d3.min(col(metric));
  100. }
  101. });
  102. const tsFormatter = getTimeFormatter(tableTimestampFormat);
  103. const div = d3.select(element);
  104. div.html('');
  105. const table = div.append('table').classed('dataframe dataframe table table-striped ' + 'table-condensed table-hover dataTable no-footer', true).attr('width', '100%');
  106. table.append('thead').append('tr').selectAll('th').data(columns.map(c => c.label)).enter().append('th').text(d => d);
  107. table.append('tbody').selectAll('tr').data(data).enter().append('tr').selectAll('td').data(row => columns.map(({
  108. key,
  109. format
  110. }) => {
  111. const val = row[key];
  112. let html;
  113. const isMetric = metrics.includes(key);
  114. if (key === '__timestamp') {
  115. html = tsFormatter(val);
  116. }
  117. if (typeof val === 'string') {
  118. html = "<span class=\"like-pre\">" + dompurify.sanitize(val) + "</span>";
  119. }
  120. if (isMetric) {
  121. html = getNumberFormatter(format)(val);
  122. }
  123. if (key[0] === '%') {
  124. html = formatPercent(val);
  125. }
  126. return {
  127. col: key,
  128. val,
  129. html,
  130. isMetric
  131. };
  132. })).enter().append('td').style('background-image', d => {
  133. if (d.isMetric) {
  134. const r = colorPositiveNegative && d.val < 0 ? 150 : 0;
  135. if (alignPositiveNegative) {
  136. const perc = Math.abs(Math.round(d.val / maxes[d.col] * 100)); // The 0.01 to 0.001 is a workaround for what appears to be a
  137. // CSS rendering bug on flat, transparent colors
  138. return "linear-gradient(to right, rgba(" + r + ",0,0,0.2), rgba(" + r + ",0,0,0.2) " + perc + "%, " + ("rgba(0,0,0,0.01) " + perc + "%, rgba(0,0,0,0.001) 100%)");
  139. }
  140. const posExtent = Math.abs(Math.max(maxes[d.col], 0));
  141. const negExtent = Math.abs(Math.min(mins[d.col], 0));
  142. const tot = posExtent + negExtent;
  143. const perc1 = Math.round(Math.min(negExtent + d.val, negExtent) / tot * 100);
  144. const perc2 = Math.round(Math.abs(d.val) / tot * 100); // The 0.01 to 0.001 is a workaround for what appears to be a
  145. // CSS rendering bug on flat, transparent colors
  146. return "linear-gradient(to right, rgba(0,0,0,0.01), rgba(0,0,0,0.001) " + perc1 + "%, " + ("rgba(" + r + ",0,0,0.2) " + perc1 + "%, rgba(" + r + ",0,0,0.2) " + (perc1 + perc2) + "%, ") + ("rgba(0,0,0,0.01) " + (perc1 + perc2) + "%, rgba(0,0,0,0.001) 100%)");
  147. }
  148. return null;
  149. }).classed('text-right', d => d.isMetric).attr('title', d => {
  150. if (typeof d.val === 'string') {
  151. return d.val;
  152. }
  153. if (!Number.isNaN(d.val)) {
  154. return formatValue(d.val);
  155. }
  156. return null;
  157. }).attr('data-sort', d => d.isMetric ? d.val : null) // Check if the dashboard currently has a filter for each row
  158. .classed('filtered', d => filters && filters[d.col] && filters[d.col].includes(d.val)).on('click', function (d) {
  159. if (!d.isMetric && tableFilter) {
  160. const td = d3.select(this);
  161. if (td.classed('filtered')) {
  162. onRemoveFilter(d.col, [d.val]);
  163. d3.select(this).classed('filtered', false);
  164. } else {
  165. d3.select(this).classed('filtered', true);
  166. onAddFilter(d.col, [d.val]);
  167. }
  168. }
  169. }).style('cursor', d => !d.isMetric ? 'pointer' : '').html(d => d.html ? d.html : d.val);
  170. const paging = pageLength && pageLength > 0;
  171. const datatable = $container.find('.dataTable').DataTable({
  172. paging,
  173. pageLength,
  174. aaSorting: [],
  175. searching: includeSearch,
  176. bInfo: false,
  177. scrollY: height + "px",
  178. scrollCollapse: true,
  179. scrollX: true
  180. });
  181. fixTableHeight($container.find('.dataTables_wrapper'), height); // Sorting table by main column
  182. let sortBy;
  183. const limitMetric = Array.isArray(timeseriesLimitMetric) ? timeseriesLimitMetric[0] : timeseriesLimitMetric;
  184. if (limitMetric) {
  185. // Sort by as specified
  186. sortBy = limitMetric.label || limitMetric;
  187. } else if (metrics.length > 0) {
  188. // If not specified, use the first metric from the list
  189. sortBy = metrics[0];
  190. }
  191. if (sortBy) {
  192. const keys = columns.map(c => c.key);
  193. const index = keys.indexOf(sortBy);
  194. datatable.column(index).order(orderDesc ? 'desc' : 'asc');
  195. if (!metrics.includes(sortBy)) {
  196. // Hiding the sortBy column if not in the metrics list
  197. datatable.column(index).visible(false);
  198. }
  199. }
  200. datatable.draw();
  201. }
  202. TableVis.displayName = 'TableVis';
  203. TableVis.propTypes = propTypes;
  204. export default TableVis;