-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat(feedback): frontend to display summary #93567
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 46 commits
1af771f
a15f6f7
9ba5ee5
3b80ac2
937f1bf
860cdb1
376de16
019c174
0a3360f
aba030a
1956230
b16637f
b34742f
7368e0f
c20e77a
4a1b805
5da9100
a22d4c6
7493363
55bfad7
6a9430d
faf5701
40dd6cc
d497108
c6e3051
d630eef
5cda5e8
43ce9a4
a451cef
72cb051
fa40586
b3a5395
ee740d1
571004b
6cd136f
e378da1
3b03f7d
5e2648e
2c525a4
f0fe0dc
2407346
04b5f2c
e383dd0
03c7aa2
f3df413
2783134
88e5262
a0ed6c3
3645bbc
59c98db
bd44743
5062dfe
ece7000
5827c79
e1e7f99
4a81d56
d6fdea2
607e8da
8232c5d
dc83c6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import {useEffect, useRef, useState} from 'react'; | ||
| import styled from '@emotion/styled'; | ||
|
|
||
| import useFeedbackSummary from 'sentry/components/feedback/list/useFeedbackSummary'; | ||
| import LoadingError from 'sentry/components/loadingError'; | ||
| import Placeholder from 'sentry/components/placeholder'; | ||
| import {IconSeer} from 'sentry/icons/iconSeer'; | ||
| import {t} from 'sentry/locale'; | ||
| import {space} from 'sentry/styles/space'; | ||
|
|
||
| export default function FeedbackSummary() { | ||
| const {error, loading, summary, tooFewFeedbacks} = useFeedbackSummary(); | ||
| const [expanded, setExpanded] = useState(false); | ||
| const [isClamped, setIsClamped] = useState(false); | ||
| const summaryRef = useRef<HTMLParagraphElement>(null); | ||
|
|
||
| useEffect(() => { | ||
| const checkClamped = () => { | ||
| const el = summaryRef.current; | ||
| if (el) { | ||
| el.style.display = '-webkit-box'; | ||
| el.style.webkitLineClamp = '2'; | ||
| const clampedHeight = el.clientHeight; | ||
|
|
||
| el.style.display = 'block'; | ||
| el.style.webkitLineClamp = 'unset'; | ||
| const fullHeight = el.clientHeight; | ||
|
|
||
| setIsClamped(fullHeight > clampedHeight); | ||
|
|
||
| // Restore to clamped state if not expanded | ||
| if (!expanded) { | ||
| el.style.display = '-webkit-box'; | ||
| el.style.webkitLineClamp = '2'; | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| checkClamped(); | ||
| const el = summaryRef.current; | ||
| if (!el) return undefined; | ||
|
|
||
| // Observe width/height changes | ||
| const resizeObserver = new window.ResizeObserver(() => { | ||
| checkClamped(); | ||
| }); | ||
| resizeObserver.observe(el); | ||
|
|
||
| // Clean up | ||
| return () => { | ||
| resizeObserver.disconnect(); | ||
| }; | ||
| }, [summary, loading, tooFewFeedbacks, expanded]); | ||
|
|
||
| if (error) { | ||
| return <LoadingError message={t('There was an error loading the summary')} />; | ||
| } | ||
vishnupsatish marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (loading) { | ||
| return <Placeholder height="100px" />; | ||
| } | ||
|
|
||
| if (tooFewFeedbacks) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <Summary> | ||
| <SummaryIconContainer> | ||
vishnupsatish marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <div> | ||
| <IconSeer size="xs" /> | ||
| </div> | ||
vishnupsatish marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <SummaryContainer> | ||
| <SummaryHeader>{t('Feedback Summary')}</SummaryHeader> | ||
| <SummaryContent | ||
| ref={summaryRef} | ||
| style={ | ||
| expanded | ||
| ? { | ||
| display: 'block', | ||
| WebkitLineClamp: 'unset', | ||
| overflow: 'visible', | ||
| } | ||
| : {} | ||
| } | ||
| > | ||
| {summary} | ||
| </SummaryContent> | ||
| {isClamped && !expanded && ( | ||
| <ReadMoreButton type="button" onClick={() => setExpanded(true)}> | ||
| {t('Read more')} | ||
| </ReadMoreButton> | ||
| )} | ||
| {expanded && isClamped && ( | ||
| <ReadMoreButton type="button" onClick={() => setExpanded(false)}> | ||
| {t('Show less')} | ||
| </ReadMoreButton> | ||
| )} | ||
| </SummaryContainer> | ||
| </SummaryIconContainer> | ||
| </Summary> | ||
| ); | ||
| } | ||
|
|
||
| const SummaryContainer = styled('div')` | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: ${space(1)}; | ||
| width: 100%; | ||
| `; | ||
|
|
||
| const SummaryHeader = styled('p')` | ||
| font-size: ${p => p.theme.fontSizeMedium}; | ||
| font-weight: ${p => p.theme.fontWeightBold}; | ||
| margin: 0; | ||
| `; | ||
|
|
||
| const SummaryContent = styled('p')` | ||
| font-size: ${p => p.theme.fontSizeSmall}; | ||
| color: ${p => p.theme.subText}; | ||
| margin: 0; | ||
| display: -webkit-box; | ||
| -webkit-line-clamp: 2; | ||
| -webkit-box-orient: vertical; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| white-space: normal; | ||
| max-width: 100%; | ||
| transition: -webkit-line-clamp 0.2s; | ||
| `; | ||
|
|
||
| const ReadMoreButton = styled('button')` | ||
| background: none; | ||
| border: none; | ||
| color: ${p => p.theme.purple400}; | ||
| cursor: pointer; | ||
| padding: 0; | ||
| font-size: ${p => p.theme.fontSizeSmall}; | ||
| font-weight: ${p => p.theme.fontWeightBold}; | ||
| align-self: flex-start; | ||
| `; | ||
|
|
||
| const Summary = styled('div')` | ||
| padding: ${space(2)}; | ||
| border: 1px solid ${p => p.theme.border}; | ||
| border-radius: ${p => p.theme.borderRadius}; | ||
| `; | ||
|
|
||
| const SummaryIconContainer = styled('div')` | ||
| display: flex; | ||
| flex-direction: row; | ||
vishnupsatish marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| gap: ${space(1)}; | ||
| `; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import {useMemo} from 'react'; | ||
|
|
||
| import useFeedbackQueryKeys from 'sentry/components/feedback/useFeedbackQueryKeys'; | ||
| import coaleseIssueStatsPeriodQuery from 'sentry/utils/feedback/coaleseIssueStatsPeriodQuery'; | ||
| import {useApiQuery} from 'sentry/utils/queryClient'; | ||
| import {decodeList, decodeScalar} from 'sentry/utils/queryString'; | ||
| import useLocationQuery from 'sentry/utils/url/useLocationQuery'; | ||
| import useOrganization from 'sentry/utils/useOrganization'; | ||
|
|
||
| type FeedbackSummaryResponse = { | ||
| num_feedbacks_used: number; | ||
vishnupsatish marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| success: boolean; | ||
| summary: string | null; | ||
| }; | ||
|
|
||
| export default function useFeedbackSummary(): { | ||
| error: Error | null; | ||
| loading: boolean; | ||
| summary: string | null; | ||
| tooFewFeedbacks: boolean | null; | ||
vishnupsatish marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } { | ||
| const queryView = useLocationQuery({ | ||
| fields: { | ||
| end: decodeScalar, | ||
| project: decodeList, | ||
| start: decodeScalar, | ||
| statsPeriod: decodeScalar, | ||
| utc: decodeScalar, | ||
| }, | ||
| }); | ||
|
|
||
| const organization = useOrganization(); | ||
|
|
||
| // This is similar to what is done in useMailboxCounts.tsx, and is also why we can't use useFeedbackSummary in feedbackListPage.tsx | ||
vishnupsatish marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const {listHeadTime} = useFeedbackQueryKeys(); | ||
|
|
||
| const queryViewWithStatsPeriod = useMemo(() => { | ||
| return coaleseIssueStatsPeriodQuery({ | ||
| defaultStatsPeriod: '0d', | ||
| listHeadTime, | ||
| prefetch: false, | ||
| queryView, | ||
| }); | ||
| }, [listHeadTime, queryView]); | ||
|
|
||
| const { | ||
| data: feedbackSummaryData, | ||
| isPending: isFeedbackSummaryLoading, | ||
| isError: isFeedbackSummaryError, | ||
| error: feedbackSummaryError, | ||
vishnupsatish marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } = useApiQuery<FeedbackSummaryResponse>( | ||
| [ | ||
| `/organizations/${organization.slug}/feedback-summary/`, | ||
| { | ||
| query: { | ||
| ...queryViewWithStatsPeriod, | ||
| }, | ||
| }, | ||
| ], | ||
| {staleTime: 5000, enabled: Boolean(queryViewWithStatsPeriod), retry: 1} | ||
vishnupsatish marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ); | ||
|
|
||
| if (isFeedbackSummaryLoading) { | ||
| return { | ||
| summary: null, | ||
| loading: true, | ||
| error: null, | ||
vishnupsatish marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| tooFewFeedbacks: false, | ||
| }; | ||
vishnupsatish marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| if (isFeedbackSummaryError) { | ||
| return { | ||
| summary: null, | ||
| loading: false, | ||
| error: feedbackSummaryError, | ||
| tooFewFeedbacks: false, | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| summary: feedbackSummaryData.summary, | ||
| loading: false, | ||
| error: null, | ||
| tooFewFeedbacks: feedbackSummaryData.num_feedbacks_used === 0, // Maybe we should surface this in the endpoint, this seems hacky | ||
|
||
| }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.