Переглянути джерело

Merge pull request #15864 from ryantxu/all-data-as-table

Use TableData for all data in react
Torkel Ödegaard 6 роки тому
батько
коміт
abd8948478

+ 0 - 2
packages/grafana-ui/src/components/Table/Table.story.tsx

@@ -40,8 +40,6 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData
       const suffix = (rowId + 1).toString();
       return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
     }),
-    type: 'table',
-    columnMap: {},
   };
 }
 

+ 3 - 17
packages/grafana-ui/src/types/data.ts

@@ -51,27 +51,13 @@ export enum NullValueMode {
 export type TimeSeriesVMs = TimeSeriesVM[];
 
 export interface Column {
-  text: string;
-  title?: string;
-  type?: string;
-  sort?: boolean;
-  desc?: boolean;
-  filterable?: boolean;
+  text: string; // The column name
+  type?: 'time' | 'number' | 'string' | 'object'; // not used anywhere? can we remove?
+  filterable?: boolean; // currently only set by elasticsearch, and used in the table panel
   unit?: string;
 }
 
 export interface TableData {
   columns: Column[];
   rows: any[];
-  type: string;
-  columnMap: any;
-}
-
-export type SingleStatValue = number | string | null;
-
-/*
- * So we can add meta info like tags & series name
- */
-export interface SingleStatValueInfo {
-  value: SingleStatValue;
 }

+ 2 - 7
packages/grafana-ui/src/types/panel.ts

@@ -1,12 +1,12 @@
 import { ComponentClass } from 'react';
-import { TimeSeries, LoadingState, TableData } from './data';
+import { LoadingState, TableData } from './data';
 import { TimeRange } from './time';
 import { ScopedVars } from './datasource';
 
 export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
 
 export interface PanelProps<T = any> {
-  panelData: PanelData;
+  data?: TableData[];
   timeRange: TimeRange;
   loading: LoadingState;
   options: T;
@@ -16,11 +16,6 @@ export interface PanelProps<T = any> {
   replaceVariables: InterpolateFunction;
 }
 
-export interface PanelData {
-  timeSeries?: TimeSeries[];
-  tableData?: TableData;
-}
-
 export interface PanelEditorProps<T = any> {
   options: T;
   onOptionsChange: (options: T) => void;

+ 0 - 4
packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap

@@ -2,7 +2,6 @@
 
 exports[`processTableData basic processing should generate a header and fix widths 1`] = `
 Object {
-  "columnMap": Object {},
   "columns": Array [
     Object {
       "text": "Column 1",
@@ -31,13 +30,11 @@ Object {
       null,
     ],
   ],
-  "type": "table",
 }
 `;
 
 exports[`processTableData basic processing should read header and two rows 1`] = `
 Object {
-  "columnMap": Object {},
   "columns": Array [
     Object {
       "text": "a",
@@ -61,6 +58,5 @@ Object {
       6,
     ],
   ],
-  "type": "table",
 }
 `;

+ 1 - 1
packages/grafana-ui/src/utils/index.ts

@@ -1,5 +1,5 @@
 export * from './processTimeSeries';
-export * from './singlestat';
+export * from './processTableData';
 export * from './valueFormats/valueFormats';
 export * from './colors';
 export * from './namedColorsPalette';

+ 39 - 1
packages/grafana-ui/src/utils/processTableData.test.ts

@@ -1,4 +1,4 @@
-import { parseCSV } from './processTableData';
+import { parseCSV, toTableData } from './processTableData';
 
 describe('processTableData', () => {
   describe('basic processing', () => {
@@ -18,3 +18,41 @@ describe('processTableData', () => {
     });
   });
 });
+
+describe('toTableData', () => {
+  it('converts timeseries to table skipping nulls', () => {
+    const input1 = {
+      target: 'Field Name',
+      datapoints: [[100, 1], [200, 2]],
+    };
+    const input2 = {
+      // without target
+      target: '',
+      datapoints: [[100, 1], [200, 2]],
+    };
+    const data = toTableData([null, input1, input2, null, null]);
+    expect(data.length).toBe(2);
+    expect(data[0].columns[0].text).toBe(input1.target);
+    expect(data[0].rows).toBe(input1.datapoints);
+
+    // Default name
+    expect(data[1].columns[0].text).toEqual('Value');
+  });
+
+  it('keeps tableData unchanged', () => {
+    const input = {
+      columns: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
+      rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]],
+    };
+    const data = toTableData([null, input, null, null]);
+    expect(data.length).toBe(1);
+    expect(data[0]).toBe(input);
+  });
+
+  it('supports null values OK', () => {
+    expect(toTableData([null, null, null, null])).toEqual([]);
+    expect(toTableData(undefined)).toEqual([]);
+    expect(toTableData((null as unknown) as any[])).toEqual([]);
+    expect(toTableData([])).toEqual([]);
+  });
+});

+ 40 - 7
packages/grafana-ui/src/utils/processTableData.ts

@@ -3,7 +3,7 @@ import isNumber from 'lodash/isNumber';
 import Papa, { ParseError, ParseMeta } from 'papaparse';
 
 // Types
-import { TableData, Column } from '../types';
+import { TableData, Column, TimeSeries } from '../types';
 
 // Subset of all parse options
 export interface TableParseOptions {
@@ -70,8 +70,6 @@ export function matchRowSizes(table: TableData): TableData {
   return {
     columns,
     rows: fixedRows,
-    type: table.type,
-    columnMap: table.columnMap,
   };
 }
 
@@ -118,8 +116,6 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
     return {
       columns: [],
       rows: [],
-      type: 'table',
-      columnMap: {},
     };
   }
 
@@ -130,11 +126,48 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
   return matchRowSizes({
     columns: makeColumns(header),
     rows: results.data,
-    type: 'table',
-    columnMap: {},
   });
 }
 
+function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData {
+  return {
+    columns: [
+      {
+        text: timeSeries.target || 'Value',
+        unit: timeSeries.unit,
+      },
+      {
+        text: 'Time',
+        type: 'time',
+        unit: 'dateTimeAsIso',
+      },
+    ],
+    rows: timeSeries.datapoints,
+  };
+}
+
+export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
+
+export const toTableData = (results?: any[]): TableData[] => {
+  if (!results) {
+    return [];
+  }
+
+  return results
+    .filter(d => !!d)
+    .map(data => {
+      if (data.hasOwnProperty('columns')) {
+        return data as TableData;
+      }
+      if (data.hasOwnProperty('datapoints')) {
+        return convertTimeSeriesToTableData(data);
+      }
+      // TODO, try to convert JSON to table?
+      console.warn('Can not convert', data);
+      throw new Error('Unsupported data format');
+    });
+};
+
 export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
   if (isNumber(sortIndex)) {
     const copy = {

+ 28 - 9
packages/grafana-ui/src/utils/processTimeSeries.ts

@@ -4,17 +4,36 @@ import isNumber from 'lodash/isNumber';
 import { colors } from './colors';
 
 // Types
-import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
+import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types';
 
 interface Options {
-  timeSeries: TimeSeries[];
+  data: TableData[];
+  xColumn?: number; // Time (or null to guess)
+  yColumn?: number; // Value (or null to guess)
   nullValueMode: NullValueMode;
 }
 
-export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
-  const vmSeries = timeSeries.map((item, index) => {
+// NOTE: this should move to processTableData.ts
+// I left it as is so the merge changes are more clear.
+export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs {
+  const vmSeries = data.map((item, index) => {
+    if (!isNumber(xColumn)) {
+      xColumn = 1; // Default timeseries colum.  TODO, find first time field!
+    }
+    if (!isNumber(yColumn)) {
+      yColumn = 0; // TODO, find first non-time field
+    }
+
+    // TODO? either % or throw error?
+    if (xColumn >= item.columns.length) {
+      throw new Error('invalid colum: ' + xColumn);
+    }
+    if (yColumn >= item.columns.length) {
+      throw new Error('invalid colum: ' + yColumn);
+    }
+
     const colorIndex = index % colors.length;
-    const label = item.target;
+    const label = item.columns[yColumn].text;
     const result = [];
 
     // stat defaults
@@ -42,9 +61,9 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
     let previousValue = 0;
     let previousDeltaUp = true;
 
-    for (let i = 0; i < item.datapoints.length; i++) {
-      currentValue = item.datapoints[i][0];
-      currentTime = item.datapoints[i][1];
+    for (let i = 0; i < item.rows.length; i++) {
+      currentValue = item.rows[i][yColumn];
+      currentTime = item.rows[i][xColumn];
 
       if (typeof currentTime !== 'number') {
         continue;
@@ -95,7 +114,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
           if (previousValue > currentValue) {
             // counter reset
             previousDeltaUp = false;
-            if (i === item.datapoints.length - 1) {
+            if (i === item.rows.length - 1) {
               // reset on last
               delta += currentValue;
             }

+ 0 - 33
packages/grafana-ui/src/utils/singlestat.ts

@@ -1,33 +0,0 @@
-import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
-import { processTimeSeries } from './processTimeSeries';
-
-export interface SingleStatProcessingOptions {
-  panelData: PanelData;
-  stat: string;
-}
-
-//
-// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
-//
-export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
-  const { panelData, stat } = options;
-
-  if (panelData.timeSeries) {
-    const timeSeries = processTimeSeries({
-      timeSeries: panelData.timeSeries,
-      nullValueMode: NullValueMode.Null,
-    });
-
-    return timeSeries.map((series, index) => {
-      const value = stat !== 'name' ? series.stats[stat] : series.label;
-
-      return {
-        value: value,
-      };
-    });
-  } else if (panelData.tableData) {
-    throw { message: 'Panel data not supported' };
-  }
-
-  return [];
-}

+ 8 - 7
public/app/core/table_model.ts

@@ -1,17 +1,18 @@
 import _ from 'lodash';
+import { Column, TableData } from '@grafana/ui';
 
-interface Column {
-  text: string;
+/**
+ * Extends the standard Column class with variables that get
+ * mutated in the angular table panel.
+ */
+interface MutableColumn extends Column {
   title?: string;
-  type?: string;
   sort?: boolean;
   desc?: boolean;
-  filterable?: boolean;
-  unit?: string;
 }
 
-export default class TableModel {
-  columns: Column[];
+export default class TableModel implements TableData {
+  columns: MutableColumn[];
   rows: any[];
   type: string;
   columnMap: any;

+ 6 - 22
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -11,16 +11,15 @@ import {
   DataQueryResponse,
   DataQueryError,
   LoadingState,
-  PanelData,
   TableData,
   TimeRange,
-  TimeSeries,
   ScopedVars,
+  toTableData,
 } from '@grafana/ui';
 
 interface RenderProps {
   loading: LoadingState;
-  panelData: PanelData;
+  data: TableData[];
 }
 
 export interface Props {
@@ -44,7 +43,7 @@ export interface State {
   isFirstLoad: boolean;
   loading: LoadingState;
   response: DataQueryResponse;
-  panelData: PanelData;
+  data?: TableData[];
 }
 
 export class DataPanel extends Component<Props, State> {
@@ -64,7 +63,6 @@ export class DataPanel extends Component<Props, State> {
       response: {
         data: [],
       },
-      panelData: {},
       isFirstLoad: true,
     };
   }
@@ -149,7 +147,7 @@ export class DataPanel extends Component<Props, State> {
       this.setState({
         loading: LoadingState.Done,
         response: resp,
-        panelData: this.getPanelData(resp),
+        data: toTableData(resp.data),
         isFirstLoad: false,
       });
     } catch (err) {
@@ -172,23 +170,9 @@ export class DataPanel extends Component<Props, State> {
     }
   };
 
-  getPanelData(response: DataQueryResponse) {
-    if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') {
-      return {
-        tableData: response.data[0] as TableData,
-        timeSeries: null,
-      };
-    }
-
-    return {
-      timeSeries: response.data as TimeSeries[],
-      tableData: null,
-    };
-  }
-
   render() {
     const { queries } = this.props;
-    const { loading, isFirstLoad, panelData } = this.state;
+    const { loading, isFirstLoad, data } = this.state;
 
     // do not render component until we have first data
     if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
@@ -206,7 +190,7 @@ export class DataPanel extends Component<Props, State> {
     return (
       <>
         {loading === LoadingState.Loading && this.renderLoadingState()}
-        {this.props.children({ loading, panelData })}
+        {this.props.children({ loading, data })}
       </>
     );
   }

+ 7 - 7
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -11,7 +11,7 @@ import { DataPanel } from './DataPanel';
 import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
 
 // Utils
-import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
+import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
 import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
 import { profiler } from 'app/core/profiler';
 import config from 'app/core/config';
@@ -19,7 +19,7 @@ import config from 'app/core/config';
 // Types
 import { DashboardModel, PanelModel } from '../state';
 import { PanelPlugin } from 'app/types';
-import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui';
+import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError, toTableData } from '@grafana/ui';
 import { ScopedVars } from '@grafana/ui';
 
 import templateSrv from 'app/features/templating/template_srv';
@@ -139,10 +139,10 @@ export class PanelChrome extends PureComponent<Props, State> {
   }
 
   get getDataForPanel() {
-    return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null;
+    return this.hasPanelSnapshot ? toTableData(this.props.panel.snapshotData) : null;
   }
 
-  renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
+  renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element {
     const { panel, plugin } = this.props;
     const { timeRange, renderCounter } = this.state;
     const PanelComponent = plugin.exports.reactPanel.panel;
@@ -157,7 +157,7 @@ export class PanelChrome extends PureComponent<Props, State> {
       <div className="panel-content">
         <PanelComponent
           loading={loading}
-          panelData={panelData}
+          data={data}
           timeRange={timeRange}
           options={panel.getOptions(plugin.exports.reactPanel.defaults)}
           width={width - 2 * config.theme.panelPadding.horizontal}
@@ -188,8 +188,8 @@ export class PanelChrome extends PureComponent<Props, State> {
             onDataResponse={this.onDataResponse}
             onError={this.onDataError}
           >
-            {({ loading, panelData }) => {
-              return this.renderPanelPlugin(loading, panelData, width, height);
+            {({ loading, data }) => {
+              return this.renderPanelPlugin(loading, data, width, height);
             }}
           </DataPanel>
         ) : (

+ 1 - 18
public/app/features/dashboard/utils/panel.ts

@@ -4,8 +4,7 @@ import store from 'app/core/store';
 // Models
 import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
 import { PanelModel } from 'app/features/dashboard/state/PanelModel';
-import { PanelData, TimeRange, TimeSeries } from '@grafana/ui';
-import { TableData } from '@grafana/ui/src';
+import { TimeRange } from '@grafana/ui';
 
 // Utils
 import { isString as _isString } from 'lodash';
@@ -170,19 +169,3 @@ export function getResolution(panel: PanelModel): number {
 
   return panel.maxDataPoints ? panel.maxDataPoints : Math.ceil(width * (panel.gridPos.w / 24));
 }
-
-const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints');
-const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
-export const snapshotDataToPanelData = (panel: PanelModel): PanelData => {
-  const snapshotData = panel.snapshotData;
-  if (isTimeSeries(snapshotData[0])) {
-    return {
-      timeSeries: snapshotData,
-    } as PanelData;
-  } else if (isTableData(snapshotData[0])) {
-    return {
-      tableData: snapshotData[0],
-    } as PanelData;
-  }
-  throw new Error('snapshotData is invalid:' + snapshotData.toString());
-};

+ 2 - 2
public/app/plugins/panel/bargauge/BarGaugePanel.tsx

@@ -32,14 +32,14 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
   };
 
   render() {
-    const { height, width, options, panelData, renderCounter } = this.props;
+    const { height, width, options, data, renderCounter } = this.props;
     return (
       <ProcessedValuesRepeater
         getProcessedValues={this.getProcessedValues}
         renderValue={this.renderValue}
         width={width}
         height={height}
-        source={panelData}
+        source={data}
         renderCounter={renderCounter}
         orientation={options.orientation}
       />

+ 2 - 2
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -37,14 +37,14 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
   };
 
   render() {
-    const { height, width, options, panelData, renderCounter } = this.props;
+    const { height, width, options, data, renderCounter } = this.props;
     return (
       <ProcessedValuesRepeater
         getProcessedValues={this.getProcessedValues}
         renderValue={this.renderValue}
         width={width}
         height={height}
-        source={panelData}
+        source={data}
         renderCounter={renderCounter}
         orientation={options.orientation}
       />

+ 3 - 3
public/app/plugins/panel/graph2/GraphPanel.tsx

@@ -16,13 +16,13 @@ interface Props extends PanelProps<Options> {}
 
 export class GraphPanel extends PureComponent<Props> {
   render() {
-    const { panelData, timeRange, width, height } = this.props;
+    const { data, timeRange, width, height } = this.props;
     const { showLines, showBars, showPoints } = this.props.options;
 
     let vmSeries: TimeSeriesVMs;
-    if (panelData.timeSeries) {
+    if (data) {
       vmSeries = processTimeSeries({
-        timeSeries: panelData.timeSeries,
+        data,
         nullValueMode: NullValueMode.Ignore,
       });
     }

+ 2 - 2
public/app/plugins/panel/singlestat/module.ts

@@ -8,7 +8,7 @@ import kbn from 'app/core/utils/kbn';
 import config from 'app/core/config';
 import TimeSeries from 'app/core/time_series2';
 import { MetricsPanelCtrl } from 'app/plugins/sdk';
-import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
+import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName, isTableData } from '@grafana/ui';
 
 class SingleStatCtrl extends MetricsPanelCtrl {
   static templateUrl = 'module.html';
@@ -112,7 +112,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
       scopedVars: _.extend({}, this.panel.scopedVars),
     };
 
-    if (dataList.length > 0 && dataList[0].type === 'table') {
+    if (dataList.length > 0 && isTableData(dataList[0])) {
       this.dataType = 'table';
       const tableData = dataList.map(this.tableHandler.bind(this));
       this.setTableValues(tableData, data);

+ 16 - 10
public/app/plugins/panel/singlestat2/SingleStatPanel.tsx

@@ -4,27 +4,33 @@ import React, { PureComponent, CSSProperties } from 'react';
 // Types
 import { SingleStatOptions, SingleStatBaseOptions } from './types';
 
-import { processSingleStatPanelData, DisplayValue, PanelProps } from '@grafana/ui';
+import { DisplayValue, PanelProps, processTimeSeries, NullValueMode } from '@grafana/ui';
 import { config } from 'app/core/config';
 import { getDisplayProcessor } from '@grafana/ui';
 import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
 
 export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): DisplayValue[] => {
-  const { panelData, replaceVariables, options } = props;
+  const { data, replaceVariables, options } = props;
   const { valueOptions, valueMappings } = options;
+  const { unit, decimals, stat } = valueOptions;
+
   const processor = getDisplayProcessor({
-    unit: valueOptions.unit,
-    decimals: valueOptions.decimals,
+    unit,
+    decimals,
     mappings: valueMappings,
     thresholds: options.thresholds,
     prefix: replaceVariables(valueOptions.prefix),
     suffix: replaceVariables(valueOptions.suffix),
     theme: config.theme,
   });
-  return processSingleStatPanelData({
-    panelData: panelData,
-    stat: valueOptions.stat,
-  }).map(stat => processor(stat.value));
+
+  return processTimeSeries({
+    data,
+    nullValueMode: NullValueMode.Null,
+  }).map((series, index) => {
+    const value = stat !== 'name' ? series.stats[stat] : series.label;
+    return processor(value);
+  });
 };
 
 export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
@@ -49,14 +55,14 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
   };
 
   render() {
-    const { height, width, options, panelData, renderCounter } = this.props;
+    const { height, width, options, data, renderCounter } = this.props;
     return (
       <ProcessedValuesRepeater
         getProcessedValues={this.getProcessedValues}
         renderValue={this.renderValue}
         width={width}
         height={height}
-        source={panelData}
+        source={data}
         renderCounter={renderCounter}
         orientation={options.orientation}
       />

+ 2 - 1
public/app/plugins/panel/table/module.ts

@@ -6,6 +6,7 @@ import { transformDataToTable } from './transformers';
 import { tablePanelEditor } from './editor';
 import { columnOptionsTab } from './column_options';
 import { TableRenderer } from './renderer';
+import { isTableData } from '@grafana/ui';
 
 class TablePanelCtrl extends MetricsPanelCtrl {
   static templateUrl = 'module.html';
@@ -104,7 +105,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
 
     // automatically correct transform mode based on data
     if (this.dataRaw && this.dataRaw.length) {
-      if (this.dataRaw[0].type === 'table') {
+      if (isTableData(this.dataRaw[0])) {
         this.panel.transform = 'table';
       } else {
         if (this.dataRaw[0].type === 'docs') {

+ 3 - 3
public/app/plugins/panel/table2/TablePanel.tsx

@@ -14,15 +14,15 @@ export class TablePanel extends Component<Props> {
   }
 
   render() {
-    const { panelData, options } = this.props;
+    const { data, options } = this.props;
 
-    if (!panelData || !panelData.tableData) {
+    if (data.length < 1) {
       return <div>No Table Data...</div>;
     }
 
     return (
       <ThemeContext.Consumer>
-        {theme => <Table {...this.props} {...options} theme={theme} data={panelData.tableData} />}
+        {theme => <Table {...this.props} {...options} theme={theme} data={data[0]} />}
       </ThemeContext.Consumer>
     );
   }