-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(feedback): Implement a feedback list page table (#56046)
Wire up a list of feedbacks and lightly style it. ![SCR-20230912-jpdj](https://github.com/getsentry/sentry/assets/187460/671064dc-fec0-4788-83b8-d2600a810267) Related to #55809
- Loading branch information
Showing
9 changed files
with
372 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import {useCallback, useMemo} from 'react'; | ||
import type {Location} from 'history'; | ||
|
||
import renderSortableHeaderCell from 'sentry/components/feedback/table/renderSortableHeaderCell'; | ||
import useQueryBasedColumnResize from 'sentry/components/feedback/table/useQueryBasedColumnResize'; | ||
import useQueryBasedSorting from 'sentry/components/feedback/table/useQueryBasedSorting'; | ||
import GridEditable, {GridColumnOrder} from 'sentry/components/gridEditable'; | ||
import Link from 'sentry/components/links/link'; | ||
import Tag from 'sentry/components/tag'; | ||
import TextOverflow from 'sentry/components/textOverflow'; | ||
import TimeSince from 'sentry/components/timeSince'; | ||
import {Tooltip} from 'sentry/components/tooltip'; | ||
import {t} from 'sentry/locale'; | ||
import {Organization} from 'sentry/types'; | ||
import {trackAnalytics} from 'sentry/utils/analytics'; | ||
import {getShortEventId} from 'sentry/utils/events'; | ||
import type { | ||
FeedbackListQueryParams, | ||
HydratedFeedbackItem, | ||
HydratedFeedbackList, | ||
} from 'sentry/utils/feedback/types'; | ||
import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes'; | ||
import useOrganization from 'sentry/utils/useOrganization'; | ||
import {useRoutes} from 'sentry/utils/useRoutes'; | ||
import {normalizeUrl} from 'sentry/utils/withDomainRequired'; | ||
|
||
interface UrlState { | ||
widths: string[]; | ||
} | ||
|
||
interface Props { | ||
data: HydratedFeedbackList; | ||
isError: boolean; | ||
isLoading: boolean; | ||
location: Location<FeedbackListQueryParams & UrlState>; | ||
} | ||
|
||
const BASE_COLUMNS: GridColumnOrder<string>[] = [ | ||
{key: 'id', name: 'id'}, | ||
{key: 'status', name: 'status'}, | ||
{key: 'contact_email', name: 'contact_email'}, | ||
{key: 'message', name: 'message'}, | ||
{key: 'replay_id', name: 'Replay'}, | ||
{key: 'timestamp', name: 'timestamp'}, | ||
]; | ||
|
||
export default function FeedbackTable({isError, isLoading, data, location}: Props) { | ||
const routes = useRoutes(); | ||
const organization = useOrganization(); | ||
|
||
const {currentSort, makeSortLinkGenerator} = useQueryBasedSorting({ | ||
defaultSort: {field: 'status', kind: 'desc'}, | ||
location, | ||
}); | ||
|
||
const {columns, handleResizeColumn} = useQueryBasedColumnResize({ | ||
columns: BASE_COLUMNS, | ||
location, | ||
}); | ||
|
||
const renderHeadCell = useMemo( | ||
() => | ||
renderSortableHeaderCell({ | ||
currentSort, | ||
makeSortLinkGenerator, | ||
onClick: () => {}, | ||
rightAlignedColumns: [], | ||
sortableColumns: columns, | ||
}), | ||
[columns, currentSort, makeSortLinkGenerator] | ||
); | ||
|
||
const renderBodyCell = useCallback( | ||
(column, dataRow) => { | ||
const value = dataRow[column.key]; | ||
switch (column.key) { | ||
case 'id': | ||
return <FeedbackDetailsLink organization={organization} value={value} />; | ||
case 'status': | ||
return <Tag type={value === 'resolved' ? 'default' : 'warning'}>{value}</Tag>; | ||
case 'message': | ||
return <TextOverflow>{value}</TextOverflow>; | ||
case 'replay_id': { | ||
const referrer = getRouteStringFromRoutes(routes); | ||
return ( | ||
<Tooltip title={t('View Replay')}> | ||
<Link | ||
to={{ | ||
pathname: normalizeUrl( | ||
`/organizations/${organization.slug}/replays/${value}/` | ||
), | ||
query: {referrer}, | ||
}} | ||
> | ||
{getShortEventId(value)} | ||
</Link> | ||
</Tooltip> | ||
); | ||
} | ||
default: | ||
return renderSimpleBodyCell<HydratedFeedbackItem>(column, dataRow); | ||
} | ||
}, | ||
[organization, routes] | ||
); | ||
|
||
return ( | ||
<GridEditable | ||
error={isError} | ||
isLoading={isLoading} | ||
data={data ?? []} | ||
columnOrder={columns} | ||
columnSortBy={[]} | ||
stickyHeader | ||
grid={{ | ||
onResizeColumn: handleResizeColumn, | ||
renderHeadCell, | ||
renderBodyCell, | ||
}} | ||
location={location as Location<any>} | ||
/> | ||
); | ||
} | ||
|
||
function FeedbackDetailsLink({ | ||
organization, | ||
value, | ||
}: { | ||
organization: Organization; | ||
value: string; | ||
}) { | ||
return ( | ||
<Link | ||
to={{ | ||
pathname: normalizeUrl(`/organizations/${organization.slug}/feedback/${value}/`), | ||
query: {referrer: 'feedback_list_page'}, | ||
}} | ||
onClick={() => { | ||
trackAnalytics('feedback_list.details_link.click', {organization}); | ||
}} | ||
> | ||
{getShortEventId(value)} | ||
</Link> | ||
); | ||
} | ||
|
||
function renderSimpleBodyCell<T>(column: GridColumnOrder<string>, dataRow: T) { | ||
const value = dataRow[column.key]; | ||
if (value instanceof Date) { | ||
return <TimeSince date={value} />; | ||
} | ||
return dataRow[column.key]; | ||
} |
26 changes: 26 additions & 0 deletions
26
static/app/components/feedback/table/queryBasedSortLinkGenerator.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import type {ReactText} from 'react'; | ||
import type {Location, LocationDescriptorObject} from 'history'; | ||
|
||
import type {GridColumnOrder} from 'sentry/components/gridEditable'; | ||
import {Sort} from 'sentry/utils/discover/fields'; | ||
|
||
export default function queryBasedSortLinkGenerator<Key extends ReactText>( | ||
location: Location, | ||
column: GridColumnOrder<Key>, | ||
currentSort: Sort | ||
): () => LocationDescriptorObject { | ||
const direction = | ||
currentSort.field !== column.key | ||
? 'desc' | ||
: currentSort.kind === 'desc' | ||
? 'asc' | ||
: 'desc'; | ||
|
||
return () => ({ | ||
...location, | ||
query: { | ||
...location.query, | ||
sort: `${direction === 'desc' ? '-' : ''}${column.key}`, | ||
}, | ||
}); | ||
} |
36 changes: 36 additions & 0 deletions
36
static/app/components/feedback/table/renderSortableHeaderCell.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import type {MouseEvent} from 'react'; | ||
import type {LocationDescriptorObject} from 'history'; | ||
|
||
import type {GridColumnOrder} from 'sentry/components/gridEditable'; | ||
import SortLink from 'sentry/components/gridEditable/sortLink'; | ||
import type {Sort} from 'sentry/utils/discover/fields'; | ||
|
||
interface Props<Key extends string> { | ||
currentSort: Sort; | ||
makeSortLinkGenerator: (column: GridColumnOrder<Key>) => () => LocationDescriptorObject; | ||
onClick(column: GridColumnOrder<Key>, e: MouseEvent<HTMLAnchorElement>): void; | ||
rightAlignedColumns: GridColumnOrder<string>[]; | ||
sortableColumns: GridColumnOrder<string>[]; | ||
} | ||
|
||
export default function renderSortableHeaderCell<Key extends string>({ | ||
currentSort, | ||
onClick, | ||
rightAlignedColumns, | ||
sortableColumns, | ||
makeSortLinkGenerator, | ||
}: Props<Key>) { | ||
return function (column: GridColumnOrder<Key>, _columnIndex: number) { | ||
return ( | ||
<SortLink | ||
onClick={e => onClick(column, e)} | ||
align={rightAlignedColumns.includes(column) ? 'right' : 'left'} | ||
title={column.name} | ||
direction={currentSort?.field === column.key ? currentSort?.kind : undefined} | ||
canSort={sortableColumns.includes(column)} | ||
generateSortLink={makeSortLinkGenerator(column)} | ||
replace | ||
/> | ||
); | ||
}; | ||
} |
45 changes: 45 additions & 0 deletions
45
static/app/components/feedback/table/useQueryBasedColumnResize.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import {useCallback, useMemo} from 'react'; | ||
import {browserHistory} from 'react-router'; | ||
import type {Location} from 'history'; | ||
import dropRightWhile from 'lodash/dropRightWhile'; | ||
|
||
import {COL_WIDTH_UNDEFINED, GridColumnOrder} from 'sentry/components/gridEditable'; | ||
import {decodeInteger, decodeList} from 'sentry/utils/queryString'; | ||
|
||
interface Props { | ||
columns: GridColumnOrder<string>[]; | ||
location: Location<{widths: string[]}>; | ||
} | ||
|
||
export default function useQueryBasedColumnResize({columns, location}: Props) { | ||
const columnsWidthWidths = useMemo(() => { | ||
const widths = decodeList(location.query.widths); | ||
|
||
return columns.map((column, i) => { | ||
column.width = decodeInteger(widths[i], COL_WIDTH_UNDEFINED); | ||
return column; | ||
}); | ||
}, [columns, location.query.widths]); | ||
|
||
const handleResizeColumn = useCallback( | ||
(columnIndex, resizedColumn) => { | ||
const widths = columns.map( | ||
(column, i) => | ||
(i === columnIndex ? resizedColumn.width : column.width) ?? COL_WIDTH_UNDEFINED | ||
); | ||
browserHistory.push({ | ||
pathname: location.pathname, | ||
query: { | ||
...location.query, | ||
widths: dropRightWhile(widths, width => width === COL_WIDTH_UNDEFINED), | ||
}, | ||
}); | ||
}, | ||
[columns, location.pathname, location.query] | ||
); | ||
|
||
return { | ||
columns: columnsWidthWidths, | ||
handleResizeColumn, | ||
}; | ||
} |
24 changes: 24 additions & 0 deletions
24
static/app/components/feedback/table/useQueryBasedSorting.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import {useMemo} from 'react'; | ||
import type {Location} from 'history'; | ||
import first from 'lodash/first'; | ||
|
||
import queryBasedSortLinkGenerator from 'sentry/components/feedback/table/queryBasedSortLinkGenerator'; | ||
import {GridColumnOrder} from 'sentry/components/gridEditable'; | ||
import {fromSorts} from 'sentry/utils/discover/eventView'; | ||
import type {Sort} from 'sentry/utils/discover/fields'; | ||
|
||
interface Props { | ||
defaultSort: Sort; | ||
location: Location<{sort?: undefined | string}>; | ||
} | ||
|
||
export default function useQueryBasedSorting({location, defaultSort}: Props) { | ||
const sorts = useMemo(() => fromSorts(location.query.sort), [location.query.sort]); | ||
const currentSort = useMemo(() => first(sorts) ?? defaultSort, [defaultSort, sorts]); | ||
|
||
return { | ||
makeSortLinkGenerator: (column: GridColumnOrder) => | ||
queryBasedSortLinkGenerator(location, column, currentSort), | ||
currentSort, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.