Skip to content
54 changes: 36 additions & 18 deletions static/app/views/dashboards/widgetCard/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {getBucketSize} from 'sentry/views/dashboards/utils/getBucketSize';
import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
import {BigNumberWidgetVisualization} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization';
import TableWidgetVisualization from 'sentry/views/dashboards/widgets/tableWidget/tableWidgetVisualization';
import {ConfidenceFooter} from 'sentry/views/explore/charts/confidenceFooter';

import type {GenericWidgetQueriesChildrenProps} from './genericWidgetQueries';
Expand Down Expand Up @@ -137,7 +138,13 @@ class WidgetCardChart extends Component<WidgetCardChartProps> {
}

tableResultComponent({loading, tableResults}: TableResultProps): React.ReactNode {
const {location, widget, selection, minTableColumnWidth} = this.props;
const {
location,
widget,
selection,
minTableColumnWidth,
organization: org,
} = this.props;
if (typeof tableResults === 'undefined') {
// Align height to other charts.
return <LoadingPlaceholder />;
Expand All @@ -162,23 +169,34 @@ class WidgetCardChart extends Component<WidgetCardChartProps> {

return (
<TableWrapper key={`table:${result.title}`}>
<StyledSimpleTableChart
eventView={eventView}
fieldAliases={fieldAliases}
location={location}
fields={fields}
title={tableResults.length > 1 ? result.title : ''}
// Bypass the loading state for span widgets because this renders the loading placeholder
// and we want to show the underlying data during preflight instead
loading={widget.widgetType === WidgetType.SPANS ? false : loading}
loader={<LoadingPlaceholder />}
metadata={result.meta}
data={result.data}
stickyHeaders
fieldHeaderMap={datasetConfig.getFieldHeaderMap?.(widget.queries[i])}
getCustomFieldRenderer={getCustomFieldRenderer}
minColumnWidth={minTableColumnWidth}
/>
{org.features.includes('use-table-widget-visualization') ? (
<TableWidgetVisualization
loading={widget.widgetType === WidgetType.SPANS ? false : loading}
columns={[]}
tableData={{
data: [],
meta: undefined,
}}
/>
) : (
<StyledSimpleTableChart
eventView={eventView}
fieldAliases={fieldAliases}
location={location}
fields={fields}
title={tableResults.length > 1 ? result.title : ''}
// Bypass the loading state for span widgets because this renders the loading placeholder
// and we want to show the underlying data during preflight instead
loading={widget.widgetType === WidgetType.SPANS ? false : loading}
loader={<LoadingPlaceholder />}
metadata={result.meta}
data={result.data}
stickyHeaders
fieldHeaderMap={datasetConfig.getFieldHeaderMap?.(widget.queries[i])}
getCustomFieldRenderer={getCustomFieldRenderer}
minColumnWidth={minTableColumnWidth}
/>
)}
</TableWrapper>
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type {Theme} from '@emotion/react';
import styled from '@emotion/styled';
import type {Location} from 'history';

import {Tooltip} from 'sentry/components/core/tooltip';
import type {GridColumnOrder} from 'sentry/components/gridEditable';
import SortLink from 'sentry/components/gridEditable/sortLink';
import type {Organization} from 'sentry/types/organization';
import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {fieldAlignment} from 'sentry/utils/discover/fields';
import type {TableColumn} from 'sentry/views/discover/table/types';

/**
* Renderers here are used as a default fallback when no renderer function is supplied
*/

interface DefaultCellRenderProps {
tableData?: TableData;
}

interface DefaultBodyCellRenderProps extends DefaultCellRenderProps {
location: Location;
organization: Organization;
theme: Theme;
}

// TODO: expand on some basic sorting functionality
export const renderDefaultHeadCell = ({tableData}: DefaultCellRenderProps) =>
function (
column: TableColumn<keyof TableDataRow>,
_columnIndex: number
): React.ReactNode {
const tableMeta = tableData?.meta;
const align = fieldAlignment(column.name, column.type, tableMeta);

return (
<SortLink
align={align}
title={<StyledTooltip title={column.name}>{column.name}</StyledTooltip>}
direction={undefined}
canSort={false}
preventScrollReset
generateSortLink={() => undefined}
/>
);
};

export const renderDefaultBodyCell = ({
tableData,
location,
organization,
theme,
}: DefaultBodyCellRenderProps) =>
function (
column: GridColumnOrder,
dataRow: Record<string, any>,
rowIndex: number,
columnIndex: number
): React.ReactNode {
const columnKey = String(column.key);
if (!tableData?.meta) {
return dataRow[column.key];
}

const fieldRenderer = getFieldRenderer(columnKey, tableData.meta, false);
const unit = tableData.meta.units?.[columnKey];

return (
<div key={`${rowIndex}-${columnIndex}:${column.name}`}>
{fieldRenderer(dataRow, {
organization,
location,
unit,
theme,
})}
</div>
);
};

const StyledTooltip = styled(Tooltip)`
display: initial;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type {TableData} from 'sentry/utils/discover/discoverQuery';
import type {TableColumn} from 'sentry/views/discover/table/types';

export const SAMPLE_TABLE_COLUMNS: Array<TableColumn<string>> = [
{
key: 'http.request_method',
name: 'http.request_method',
type: 'never',
isSortable: false,
column: {
kind: 'field',
field: 'http.request_method',
alias: '',
},
width: -1,
},
{
key: 'count(span.duration)',
name: 'count(span.duration)',
type: 'number',
isSortable: true,
column: {
kind: 'function',
function: ['count', 'span.duration', undefined, undefined],
alias: '',
},
width: -1,
},
];

export const SAMPLE_TABLE_DATA: TableData = {
data: [
{
'http.request_method': 'PATCH',
'count(span.duration)': 14105,
id: '',
},
{
'http.request_method': 'HEAD',
'count(span.duration)': 9494,
id: '',
},
{
'http.request_method': 'GET',
'count(span.duration)': 38583495,
id: '',
},
{
'http.request_method': 'DELETE',
'count(span.duration)': 123,
id: '',
},
{
'http.request_method': 'POST',
'count(span.duration)': 21313,
id: '',
},
],
meta: {
'http.request_method': 'string',
'count(span.duration)': 'integer',
units: {
'http.request_method': '',
'count(span.duration)': '',
},
isMetricsData: false,
isMetricsExtractedData: false,
datasetReason: 'unchanged',
dataset: 'spans',
dataScanned: 'partial',
accuracy: {
confidence: [
{
'count(span.duration)': 'high',
},
{
'count(span.duration)': 'low',
},
{
'count(span.duration)': 'high',
},
{
'count(span.duration)': 'high',
},
{
'count(span.duration)': 'high',
},
],
},
fields: {
'http.request_method': 'string',
'count(span.duration)': 'integer',
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {Fragment} from 'react';

import * as Storybook from 'sentry/stories';
import {
SAMPLE_TABLE_COLUMNS,
SAMPLE_TABLE_DATA,
} from 'sentry/views/dashboards/widgets/tableWidget/fixtures/sampleTableProps';
import TableWidgetVisualization from 'sentry/views/dashboards/widgets/tableWidget/tableWidgetVisualization';

export default Storybook.story('TableWidgetVisualization', story => {
story('Getting Started', () => {
return (
<Fragment>
<p>
<Storybook.JSXNode name="TableWidgetVisualization" /> is meant to be a robust
and eventual replacement to all tables in Dashboards and Insights (and
potentially more). The inner component of this table is{' '}
<Storybook.JSXNode name="GridEditable" />.
</p>
<p>
Below is the the most basic example of the table which requires
<code>columns</code> and <code>tableData</code> populating the table headers and
table body respectively.
</p>
<TableWidgetVisualization
loading={false}
tableData={SAMPLE_TABLE_DATA}
columns={SAMPLE_TABLE_COLUMNS}
/>
</Fragment>
);
});

story('Table Columns and Table Data', () => {
return (
<Fragment>
<p>
Currently, the columns use the type <code>TableColumn[]</code> and are rendered
in the order they are supplied. The table data uses the type{' '}
<code>TableData</code>.
</p>
</Fragment>
);
});

story('Using Custom Cell Rendering', () => {
return (
<Fragment>
<p>By default, the table falls back on predefined default rendering functions.</p>
<p>
If custom cell rendering is required, pass
<Storybook.JSXProperty name="renderTableBodyCell" value="function" /> and{' '}
<Storybook.JSXProperty name="renderTableHeadCell" value="function" /> which
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this a little hard to understand because I thought it meant that we needed to pass the string literal "function" as the value for these props. Can we reword it to just mention passing a function that returns a component?

replace the rendering of table body cells and table headers respectively
</p>
<p>Ex. (to update...)</p>
<TableWidgetVisualization
loading={false}
tableData={SAMPLE_TABLE_DATA}
columns={SAMPLE_TABLE_COLUMNS}
/>
</Fragment>
);
});

story('Custom Styling', () => {
return (
<Fragment>
<p>
The underlying <Storybook.JSXNode name="GridEditable" /> component allows for
several useful styling props to be used to format the table. Similarly, this
table allow allows for users to pass any overriding styles.
</p>
<p>Ex. we can pass custom styles to remove the border of the table:</p>
<TableWidgetVisualization
loading={false}
tableData={SAMPLE_TABLE_DATA}
columns={SAMPLE_TABLE_COLUMNS}
style={{border: 'none'}}
/>
</Fragment>
);
});
});
Loading
Loading