Skip to content

Commit

Permalink
feat(dashboards): Widget Viewer style improvements and use GridEditab…
Browse files Browse the repository at this point in the history
…le component (#32051)

Changes to some of the style in the Widget Viewer.
Also updates the Widget Viewer to use the GridEditable component to enable resizing headers.

This change also allows us to enable sort by clicking column headers, which will be completed in another PR.
Pagination is also todo
  • Loading branch information
edwardgou-sentry authored Mar 1, 2022
1 parent d2cab2d commit ec4f469
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 78 deletions.
192 changes: 118 additions & 74 deletions static/app/components/modals/widgetViewerModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,28 @@ import cloneDeep from 'lodash/cloneDeep';
import {ModalRenderProps} from 'sentry/actionCreators/modal';
import Button from 'sentry/components/button';
import ButtonBar from 'sentry/components/buttonBar';
import SimpleTableChart from 'sentry/components/charts/simpleTableChart';
import GridEditable, {GridColumnOrder} from 'sentry/components/gridEditable';
import {t} from 'sentry/locale';
import space from 'sentry/styles/space';
import {Organization, PageFilters} from 'sentry/types';
import useApi from 'sentry/utils/useApi';
import withPageFilters from 'sentry/utils/withPageFilters';
import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboardsV2/types';
import {
eventViewFromWidget,
getFieldsFromEquations,
getWidgetDiscoverUrl,
getWidgetIssueUrl,
} from 'sentry/views/dashboardsV2/utils';
import IssueWidgetQueries from 'sentry/views/dashboardsV2/widgetCard/issueWidgetQueries';
import WidgetCardChartContainer from 'sentry/views/dashboardsV2/widgetCard/widgetCardChartContainer';
import WidgetQueries from 'sentry/views/dashboardsV2/widgetCard/widgetQueries';

import {
renderGridBodyCell,
renderGridHeaderCell,
} from './widgetViewerModal/widgetViewerTableCell';

export type WidgetViewerModalOptions = {
organization: Organization;
widget: Widget;
Expand All @@ -35,30 +42,15 @@ type Props = ModalRenderProps &
selection: PageFilters;
};

const TABLE_ITEM_LIMIT = 30;
const FULL_TABLE_HEIGHT = 600;
const HALF_TABLE_HEIGHT = 300;
const FULL_TABLE_ITEM_LIMIT = 20;
const HALF_TABLE_ITEM_LIMIT = 10;
const GEO_COUNTRY_CODE = 'geo.country_code';

function WidgetViewerModal(props: Props) {
const {organization, widget, selection, location, Footer, Body, Header, onEdit} = props;
const isTableWidget = widget.displayType === DisplayType.TABLE;
const renderWidgetViewer = () => {
const {organization, selection, widget, location} = props;
const api = useApi();
switch (widget.displayType) {
case DisplayType.TABLE:
return (
<TableContainer height={FULL_TABLE_HEIGHT}>
<WidgetCardChartContainer
api={api}
organization={organization}
selection={selection}
widget={widget}
tableItemLimit={TABLE_ITEM_LIMIT}
/>
</TableContainer>
);
default:
}

// Create Table widget
const tableWidget = {...cloneDeep(widget), displayType: DisplayType.TABLE};
Expand All @@ -71,57 +63,114 @@ function WidgetViewerModal(props: Props) {
) {
fields.unshift(GEO_COUNTRY_CODE);
}

// Updates fields by adding any individual terms from equation fields as a column
const equationFields = getFieldsFromEquations(fields);
equationFields.forEach(term => {
if (Array.isArray(fields) && !fields.includes(term)) {
fields.unshift(term);
}
});
if (!isTableWidget) {
// Updates fields by adding any individual terms from equation fields as a column
const equationFields = getFieldsFromEquations(fields);
equationFields.forEach(term => {
if (Array.isArray(fields) && !fields.includes(term)) {
fields.unshift(term);
}
});
}
const eventView = eventViewFromWidget(
tableWidget.title,
tableWidget.queries[0],
selection,
tableWidget.displayType
);
const columnOrder = eventView.getColumns();
const columnSortBy = eventView.getSorts();
return (
<React.Fragment>
<Container>
<WidgetCardChartContainer
api={api}
organization={organization}
selection={selection}
widget={widget}
/>
</Container>
<TableContainer height={HALF_TABLE_HEIGHT}>
<WidgetQueries
api={api}
organization={organization}
widget={tableWidget}
selection={selection}
limit={TABLE_ITEM_LIMIT}
>
{({tableResults, loading}) => {
return (
<StyledSimpleTableChart
location={location}
title=""
fields={tableWidget.queries[0].fields}
loading={loading}
metadata={tableResults?.[0]?.meta}
data={tableResults?.[0]?.data}
organization={organization}
topResultsIndicators={
widget.displayType === DisplayType.TOP_N ? 5 : undefined
}
stickyHeaders
/>
);
}}
</WidgetQueries>
{widget.displayType !== DisplayType.TABLE && (
<Container>
<WidgetCardChartContainer
api={api}
organization={organization}
selection={selection}
widget={widget}
/>
</Container>
)}
<TableContainer>
{widget.widgetType === WidgetType.ISSUE ? (
<IssueWidgetQueries
api={api}
organization={organization}
widget={tableWidget}
selection={selection}
limit={
widget.displayType === DisplayType.TABLE
? FULL_TABLE_ITEM_LIMIT
: HALF_TABLE_ITEM_LIMIT
}
>
{({transformedResults, loading}) => {
return (
<GridEditable
isLoading={loading}
data={transformedResults}
columnOrder={columnOrder}
columnSortBy={columnSortBy}
grid={{
renderHeadCell: renderGridHeaderCell({
...props,
}) as (
column: GridColumnOrder,
columnIndex: number
) => React.ReactNode,
renderBodyCell: renderGridBodyCell({
...props,
}),
}}
location={location}
/>
);
}}
</IssueWidgetQueries>
) : (
<WidgetQueries
api={api}
organization={organization}
widget={tableWidget}
selection={selection}
limit={
widget.displayType === DisplayType.TABLE
? FULL_TABLE_ITEM_LIMIT
: HALF_TABLE_ITEM_LIMIT
}
>
{({tableResults, loading}) => {
return (
<GridEditable
isLoading={loading}
data={tableResults?.[0]?.data ?? []}
columnOrder={columnOrder}
columnSortBy={columnSortBy}
grid={{
renderHeadCell: renderGridHeaderCell({
...props,
tableData: tableResults?.[0],
}) as (
column: GridColumnOrder,
columnIndex: number
) => React.ReactNode,
renderBodyCell: renderGridBodyCell({
...props,
tableData: tableResults?.[0],
}),
}}
location={location}
/>
);
}}
</WidgetQueries>
)}
</TableContainer>
</React.Fragment>
);
};

const {Footer, Body, Header, widget, onEdit, selection, organization} = props;

const StyledHeader = styled(Header)`
${headerCss}
`;
Expand Down Expand Up @@ -165,7 +214,6 @@ function WidgetViewerModal(props: Props) {
export const modalCss = css`
width: 100%;
max-width: 1400px;
margin: 70px auto;
`;

const headerCss = css`
Expand All @@ -186,21 +234,17 @@ const Container = styled('div')`
`;

// Table Container allows Table display to work around parent padding and fill full modal width
const TableContainer = styled('div')<{height: number}>`
height: ${p => p.height}px;
width: calc(100% + 60px);
const TableContainer = styled('div')`
max-width: 1400px;
position: relative;
left: -${space(4)};
margin: ${space(4)} 0;
& > div {
max-height: ${p => p.height}px;
margin: 0;
}
`;
const StyledSimpleTableChart = styled(SimpleTableChart)`
box-shadow: none;
& td:first-child {
padding: ${space(1)} ${space(2)};
}
`;

export default withRouter(withPageFilters(WidgetViewerModal));
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as React from 'react';
import styled from '@emotion/styled';
import {Location, LocationDescriptorObject} from 'history';

import {GridColumnOrder} from 'sentry/components/gridEditable';
import SortLink from 'sentry/components/gridEditable/sortLink';
import Tooltip from 'sentry/components/tooltip';
import Truncate from 'sentry/components/truncate';
import {Organization, PageFilters} from 'sentry/types';
import {defined} from 'sentry/utils';
import {getIssueFieldRenderer} from 'sentry/utils/dashboards/issueFieldRenderers';
import {TableDataRow, TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
import {isFieldSortable} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {
fieldAlignment,
getAggregateAlias,
getEquationAliasIndex,
isEquationAlias,
} from 'sentry/utils/discover/fields';
import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboardsV2/types';
import {eventViewFromWidget} from 'sentry/views/dashboardsV2/utils';
import {ISSUE_FIELDS} from 'sentry/views/dashboardsV2/widgetBuilder/issueWidget/fields';
import TopResultsIndicator from 'sentry/views/eventsV2/table/topResultsIndicator';
import {TableColumn} from 'sentry/views/eventsV2/table/types';

// Dashboards only supports top 5 for now
const DEFAULT_NUM_TOP_EVENTS = 5;

type Props = {
location: Location;
organization: Organization;
selection: PageFilters;
widget: Widget;
tableData?: TableDataWithTitle;
};

export const renderGridHeaderCell =
({selection, widget, tableData}: Props) =>
(column: TableColumn<keyof TableDataRow>, _columnIndex: number): React.ReactNode => {
const eventView = eventViewFromWidget(
widget.title,
widget.queries[0],
selection,
widget.displayType
);
const tableMeta = tableData?.meta;
const align = fieldAlignment(column.name, column.type, tableMeta);
const field = {field: column.name, width: column.width};
function generateSortLink(): LocationDescriptorObject | undefined {
// TODO: need write sort link generation for widget viewer
return undefined;
}
const currentSort = eventView.sortForField(field, tableMeta);
const canSort = isFieldSortable(field, tableMeta);
const titleText = isEquationAlias(column.name)
? eventView.getEquations()[getEquationAliasIndex(column.name)]
: column.name;

return (
<SortLink
align={align}
title={
<StyledTooltip title={titleText}>
<Truncate value={titleText} maxLength={60} expandable={false} />
</StyledTooltip>
}
direction={currentSort ? currentSort.kind : undefined}
canSort={canSort}
generateSortLink={generateSortLink}
/>
);
};

export const renderGridBodyCell =
({location, organization, widget, tableData}: Props) =>
(
column: GridColumnOrder,
dataRow: Record<string, any>,
rowIndex: number,
columnIndex: number
): React.ReactNode => {
const columnKey = String(column.key);
const isTopEvents = widget.displayType === DisplayType.TOP_N;
let cell: React.ReactNode;
switch (widget.widgetType) {
case WidgetType.ISSUE:
cell = (
getIssueFieldRenderer(columnKey) ?? getFieldRenderer(columnKey, ISSUE_FIELDS)
)(dataRow, {organization, location});
break;
case WidgetType.DISCOVER:
default:
if (!tableData || !tableData.meta) {
return dataRow[column.key];
}
cell = getFieldRenderer(columnKey, tableData.meta)(dataRow, {
organization,
location,
});

const fieldName = getAggregateAlias(columnKey);
const value = dataRow[fieldName];
if (tableData.meta[fieldName] === 'integer' && defined(value) && value > 999) {
return (
<Tooltip
title={value.toLocaleString()}
containerDisplayMode="block"
position="right"
>
{cell}
</Tooltip>
);
}
break;
}

return (
<React.Fragment>
{isTopEvents && rowIndex < DEFAULT_NUM_TOP_EVENTS && columnIndex === 0 ? (
<TopResultsIndicator count={DEFAULT_NUM_TOP_EVENTS} index={rowIndex} />
) : null}
{cell}
</React.Fragment>
);
};

const StyledTooltip = styled(Tooltip)`
display: initial;
`;
Loading

0 comments on commit ec4f469

Please sign in to comment.