|
@@ -1,11 +1,13 @@
|
|
|
import {Fragment, useEffect, useState} from 'react';
|
|
|
import styled from '@emotion/styled';
|
|
|
import {Location, LocationDescriptorObject} from 'history';
|
|
|
+import isEqual from 'lodash/isEqual';
|
|
|
import omit from 'lodash/omit';
|
|
|
import * as qs from 'query-string';
|
|
|
|
|
|
import GuideAnchor from 'sentry/components/assistant/guideAnchor';
|
|
|
import {Button} from 'sentry/components/button';
|
|
|
+import Checkbox from 'sentry/components/checkbox';
|
|
|
import GridEditable, {
|
|
|
COL_WIDTH_UNDEFINED,
|
|
|
GridColumn,
|
|
@@ -29,6 +31,7 @@ import DiscoverQuery, {
|
|
|
import EventView, {isFieldSortable, MetaType} from 'sentry/utils/discover/eventView';
|
|
|
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
|
|
|
import {getAggregateAlias, RateUnits} from 'sentry/utils/discover/fields';
|
|
|
+import TopResultsIndicator from 'sentry/views/discover/table/topResultsIndicator';
|
|
|
import {TableColumn} from 'sentry/views/discover/table/types';
|
|
|
import {ThroughputCell} from 'sentry/views/starfish/components/tableCells/throughputCell';
|
|
|
import {TimeSpentCell} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
|
|
@@ -47,23 +50,40 @@ const COLUMN_TITLES = [
|
|
|
|
|
|
type Props = {
|
|
|
eventView: EventView;
|
|
|
+ inactiveTransactions: string[];
|
|
|
location: Location;
|
|
|
organization: Organization;
|
|
|
setError: (msg: string | undefined) => void;
|
|
|
+ setInactiveTransactions: (endpoints: string[]) => void;
|
|
|
+ setTransactionsList: (endpoints: string[]) => void;
|
|
|
+ transactionsList: string[];
|
|
|
};
|
|
|
|
|
|
export type TableColumnHeader = GridColumnHeader<keyof TableDataRow> & {
|
|
|
column?: TableColumn<keyof TableDataRow>['column']; // TODO - remove this once gridEditable is properly typed
|
|
|
};
|
|
|
|
|
|
-function QueryIssueCounts({eventView, tableData, children}) {
|
|
|
+function QueryIssueCounts({
|
|
|
+ setTransactionsList,
|
|
|
+ transactionsList,
|
|
|
+ eventView,
|
|
|
+ tableData,
|
|
|
+ children,
|
|
|
+}) {
|
|
|
const transactions: Map<string, string> = new Map();
|
|
|
+ const newTransactionsList: string[] = [];
|
|
|
transactions.set('is:unresolved', '');
|
|
|
if (tableData) {
|
|
|
tableData.data.forEach(row => {
|
|
|
transactions.set(`transaction:${row.transaction} is:unresolved`, row.transaction);
|
|
|
+ newTransactionsList.push(row.transaction);
|
|
|
});
|
|
|
}
|
|
|
+ useEffect(() => {
|
|
|
+ if (!isEqual(transactionsList, newTransactionsList)) {
|
|
|
+ setTransactionsList(newTransactionsList);
|
|
|
+ }
|
|
|
+ });
|
|
|
const {data, isLoading} = useIssueCounts(eventView, Array.from(transactions.keys()));
|
|
|
const result: Map<string, IssueCounts> = new Map();
|
|
|
for (const [query, count] of data ? Object.entries(data) : []) {
|
|
@@ -75,7 +95,16 @@ function QueryIssueCounts({eventView, tableData, children}) {
|
|
|
return children({issueCounts: result, isIssueLoading: isLoading});
|
|
|
}
|
|
|
|
|
|
-function EndpointList({eventView, location, organization, setError}: Props) {
|
|
|
+function EndpointList({
|
|
|
+ eventView,
|
|
|
+ location,
|
|
|
+ organization,
|
|
|
+ setError,
|
|
|
+ setTransactionsList,
|
|
|
+ transactionsList,
|
|
|
+ inactiveTransactions,
|
|
|
+ setInactiveTransactions,
|
|
|
+}: Props) {
|
|
|
const [widths, setWidths] = useState<number[]>([]);
|
|
|
const [_eventView, setEventView] = useState<EventView>(eventView);
|
|
|
const overallEventView = _eventView.clone();
|
|
@@ -109,11 +138,37 @@ function EndpointList({eventView, location, organization, setError}: Props) {
|
|
|
const field = String(column.key);
|
|
|
const fieldRenderer = getFieldRenderer(field, tableMeta, false);
|
|
|
const rendered = fieldRenderer(dataRow, {organization, location});
|
|
|
+ const index = tableData.data.indexOf(dataRow);
|
|
|
+
|
|
|
+ function toggleCheckbox(transaction: string) {
|
|
|
+ let newInactiveTransactions = inactiveTransactions.slice();
|
|
|
+ if (inactiveTransactions.indexOf(transaction) === -1) {
|
|
|
+ newInactiveTransactions.push(transaction);
|
|
|
+ } else {
|
|
|
+ newInactiveTransactions = inactiveTransactions.filter(
|
|
|
+ name => name !== transaction
|
|
|
+ );
|
|
|
+ }
|
|
|
+ setInactiveTransactions(newInactiveTransactions);
|
|
|
+ }
|
|
|
|
|
|
if (field === 'transaction') {
|
|
|
const method = dataRow['http.method'];
|
|
|
if (method === undefined && dataRow.transaction === 'Overall') {
|
|
|
- return <span>{dataRow.transaction}</span>;
|
|
|
+ return (
|
|
|
+ <span>
|
|
|
+ <TopResultsIndicator count={tableData.data.length + 2} index={6} />
|
|
|
+ <TransactionColumn>
|
|
|
+ <TransactionCheckbox
|
|
|
+ checked={
|
|
|
+ inactiveTransactions.indexOf(dataRow.transaction as string) === -1
|
|
|
+ }
|
|
|
+ onChange={() => toggleCheckbox(dataRow.transaction as string)}
|
|
|
+ />
|
|
|
+ <TransactionText>{dataRow.transaction}</TransactionText>
|
|
|
+ </TransactionColumn>
|
|
|
+ </span>
|
|
|
+ );
|
|
|
}
|
|
|
const endpointName =
|
|
|
method && !dataRow.transaction.toString().startsWith(method.toString())
|
|
@@ -121,27 +176,34 @@ function EndpointList({eventView, location, organization, setError}: Props) {
|
|
|
: dataRow.transaction;
|
|
|
|
|
|
return (
|
|
|
- <Link
|
|
|
- to={`/organizations/${
|
|
|
- organization.slug
|
|
|
- }/starfish/endpoint-overview/?${qs.stringify({
|
|
|
- endpoint: dataRow.transaction,
|
|
|
- 'http.method': dataRow['http.method'],
|
|
|
- statsPeriod: eventView.statsPeriod,
|
|
|
- project: eventView.project,
|
|
|
- start: eventView.start,
|
|
|
- end: eventView.end,
|
|
|
- })}`}
|
|
|
- style={{display: `block`, width: `100%`}}
|
|
|
- onClick={() => {
|
|
|
- trackAnalytics('starfish.web_service_view.endpoint_list.endpoint.clicked', {
|
|
|
- organization,
|
|
|
+ <TransactionColumn>
|
|
|
+ <TransactionCheckbox
|
|
|
+ checked={inactiveTransactions.indexOf(dataRow.transaction as string) === -1}
|
|
|
+ onChange={() => toggleCheckbox(dataRow.transaction as string)}
|
|
|
+ />
|
|
|
+ <Link
|
|
|
+ to={`/organizations/${
|
|
|
+ organization.slug
|
|
|
+ }/starfish/endpoint-overview/?${qs.stringify({
|
|
|
endpoint: dataRow.transaction,
|
|
|
- });
|
|
|
- }}
|
|
|
- >
|
|
|
- {endpointName}
|
|
|
- </Link>
|
|
|
+ 'http.method': dataRow['http.method'],
|
|
|
+ statsPeriod: eventView.statsPeriod,
|
|
|
+ project: eventView.project,
|
|
|
+ start: eventView.start,
|
|
|
+ end: eventView.end,
|
|
|
+ })}`}
|
|
|
+ style={{display: `block`, width: `100%`}}
|
|
|
+ onClick={() => {
|
|
|
+ trackAnalytics('starfish.web_service_view.endpoint_list.endpoint.clicked', {
|
|
|
+ organization,
|
|
|
+ endpoint: dataRow.transaction,
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <TopResultsIndicator count={tableData.data.length + 2} index={index} />
|
|
|
+ <TransactionText>{endpointName}</TransactionText>
|
|
|
+ </Link>
|
|
|
+ </TransactionColumn>
|
|
|
);
|
|
|
}
|
|
|
|
|
@@ -381,7 +443,12 @@ function EndpointList({eventView, location, organization, setError}: Props) {
|
|
|
>
|
|
|
{({pageLinks, isLoading, tableData}) => (
|
|
|
<Fragment>
|
|
|
- <QueryIssueCounts eventView={eventView} tableData={tableData}>
|
|
|
+ <QueryIssueCounts
|
|
|
+ setTransactionsList={setTransactionsList}
|
|
|
+ transactionsList={transactionsList}
|
|
|
+ eventView={eventView}
|
|
|
+ tableData={tableData}
|
|
|
+ >
|
|
|
{({issueCounts, isIssueLoading}) => (
|
|
|
<GridEditable
|
|
|
isLoading={isLoading && isIssueLoading}
|
|
@@ -418,6 +485,19 @@ const StyledSearchBar = styled(BaseSearchBar)`
|
|
|
margin-bottom: ${space(2)};
|
|
|
`;
|
|
|
|
|
|
+const TransactionColumn = styled('div')`
|
|
|
+ display: flex;
|
|
|
+`;
|
|
|
+
|
|
|
+const TransactionText = styled('div')`
|
|
|
+ margin-top: ${space(0.5)};
|
|
|
+`;
|
|
|
+
|
|
|
+const TransactionCheckbox = styled(Checkbox)`
|
|
|
+ top: ${space(0.5)};
|
|
|
+ margin-right: ${space(1)};
|
|
|
+`;
|
|
|
+
|
|
|
const IssueButton = styled(Button)`
|
|
|
width: 100%;
|
|
|
margin-left: auto;
|