Skip to content

Commit a54e787

Browse files
Merge branch 'main' into cursor/fix-lucene-query-truncation-5f60
2 parents 87e136f + 74b59a6 commit a54e787

5 files changed

Lines changed: 375 additions & 4 deletions

File tree

packages/app/src/components/AppNav/AppNav.module.scss

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,23 @@ $transition-slow: 0.2s ease;
449449
padding-bottom: $spacing-sm;
450450
}
451451

452+
/* Feedback */
453+
454+
.feedbackLabel {
455+
@include text-truncate;
456+
457+
line-height: 1.2;
458+
}
459+
460+
.feedbackHide {
461+
cursor: pointer;
462+
flex-shrink: 0;
463+
464+
&:hover {
465+
color: var(--color-text-sidenav-link-active);
466+
}
467+
}
468+
452469
/* Scrollbar Customization */
453470

454471
.scrollbar {

packages/app/src/components/AppNav/AppNav.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
AppNavLink,
5050
AppNavUserMenu,
5151
} from './AppNav.components';
52+
import { AppNavFeedback } from './AppNavFeedback';
5253

5354
import styles from './AppNav.module.scss';
5455

@@ -476,6 +477,9 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
476477
{/* Help */}
477478
<AppNavHelpMenu version={APP_VERSION} />
478479

480+
{/* Feedback */}
481+
<AppNavFeedback />
482+
479483
{/* Team Settings (Cloud only) */}
480484
{!IS_LOCAL_MODE && (
481485
<AppNavLink
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import React, { useCallback, useState } from 'react';
2+
import { useRouter } from 'next/router';
3+
import HyperDX from '@hyperdx/browser';
4+
import {
5+
ActionIcon,
6+
Box,
7+
Button,
8+
Group,
9+
Text,
10+
Textarea,
11+
Tooltip,
12+
} from '@mantine/core';
13+
import { useLocalStorage } from '@mantine/hooks';
14+
import {
15+
IconThumbDown,
16+
IconThumbDownFilled,
17+
IconThumbUp,
18+
IconThumbUpFilled,
19+
} from '@tabler/icons-react';
20+
21+
import { IS_LOCAL_MODE } from '@/config';
22+
23+
import { AppNavContext } from './AppNav.components';
24+
25+
import styles from './AppNav.module.scss';
26+
27+
type FeedbackVote = 'up' | 'down' | null;
28+
29+
type FeedbackState = 'idle' | 'voted' | 'thanks';
30+
31+
const FORCE_ENABLE_KEY = 'hdx-feedback-enabled';
32+
33+
class FeedbackErrorBoundary extends React.Component<
34+
{ children: React.ReactNode },
35+
{ hasError: boolean }
36+
> {
37+
state = { hasError: false };
38+
39+
static getDerivedStateFromError() {
40+
return { hasError: true };
41+
}
42+
43+
render() {
44+
if (this.state.hasError) return null;
45+
return this.props.children;
46+
}
47+
}
48+
49+
export const AppNavFeedback = () => (
50+
<FeedbackErrorBoundary>
51+
<AppNavFeedbackInner />
52+
</FeedbackErrorBoundary>
53+
);
54+
55+
const AppNavFeedbackInner = () => {
56+
const { isCollapsed } = React.useContext(AppNavContext);
57+
const [forceEnabled] = useLocalStorage<boolean>({
58+
key: FORCE_ENABLE_KEY,
59+
defaultValue: false,
60+
});
61+
const [hidden, setHidden] = useLocalStorage<boolean>({
62+
key: 'feedbackHidden',
63+
defaultValue: false,
64+
});
65+
66+
const [vote, setVote] = useState<FeedbackVote>(null);
67+
const [comment, setComment] = useState('');
68+
const [state, setState] = useState<FeedbackState>('idle');
69+
const router = useRouter();
70+
71+
// Only show when HyperDX SDK is active (non-local mode),
72+
// or when overridden via: localStorage.setItem('hdx-feedback-enabled', 'true')
73+
const sdkEnabled = !IS_LOCAL_MODE || forceEnabled === true;
74+
75+
const reset = useCallback(() => {
76+
setVote(null);
77+
setComment('');
78+
setState('idle');
79+
}, []);
80+
81+
const pageContext = useCallback(
82+
() => ({
83+
page: router.pathname,
84+
route: router.asPath,
85+
}),
86+
[router],
87+
);
88+
89+
const handleVote = useCallback(
90+
(newVote: FeedbackVote) => {
91+
setVote(newVote);
92+
setState('voted');
93+
HyperDX.addAction('user feedback vote', {
94+
vote: newVote ?? '',
95+
...pageContext(),
96+
});
97+
},
98+
[setVote, setState, pageContext],
99+
);
100+
101+
const [dismissed, setDismissed] = useState(false);
102+
103+
const handleSubmit = useCallback(() => {
104+
HyperDX.addAction('user feedback comment', {
105+
vote: vote ?? '',
106+
comment,
107+
...pageContext(),
108+
});
109+
110+
setState('thanks');
111+
setTimeout(() => {
112+
reset();
113+
setDismissed(true);
114+
}, 1500);
115+
}, [vote, comment, pageContext, reset]);
116+
117+
if (!sdkEnabled || hidden || dismissed) return null;
118+
119+
if (isCollapsed) {
120+
return (
121+
<Tooltip label="Feedback" position="right">
122+
<Group
123+
data-testid="feedback-inline"
124+
gap={0}
125+
justify="center"
126+
py={4}
127+
wrap="nowrap"
128+
>
129+
<ActionIcon
130+
data-testid="feedback-thumbs-up"
131+
variant="subtle"
132+
size="sm"
133+
onClick={() => handleVote('up')}
134+
title="Thumbs up"
135+
>
136+
<IconThumbUp size={14} />
137+
</ActionIcon>
138+
<ActionIcon
139+
data-testid="feedback-thumbs-down"
140+
variant="subtle"
141+
size="sm"
142+
onClick={() => handleVote('down')}
143+
title="Thumbs down"
144+
>
145+
<IconThumbDown size={14} />
146+
</ActionIcon>
147+
</Group>
148+
</Tooltip>
149+
);
150+
}
151+
152+
return (
153+
<Box data-testid="feedback-inline">
154+
{state === 'thanks' ? (
155+
<Text
156+
size="xs"
157+
c="dimmed"
158+
data-testid="feedback-thanks"
159+
className={styles.feedbackLabel}
160+
px="lg"
161+
py={4}
162+
>
163+
Thanks for your feedback!
164+
</Text>
165+
) : (
166+
<>
167+
<Group
168+
gap={6}
169+
wrap="nowrap"
170+
align="center"
171+
className={styles.navItem}
172+
>
173+
<span className={styles.navItemContent}>
174+
<span className={styles.navItemIcon}>
175+
<ActionIcon
176+
data-testid="feedback-thumbs-up"
177+
variant={vote === 'up' ? 'secondary' : 'subtle'}
178+
size="xs"
179+
onClick={() => handleVote('up')}
180+
title="Thumbs up"
181+
>
182+
{vote === 'up' ? (
183+
<IconThumbUpFilled size={14} />
184+
) : (
185+
<IconThumbUp size={14} />
186+
)}
187+
</ActionIcon>
188+
</span>
189+
<ActionIcon
190+
data-testid="feedback-thumbs-down"
191+
variant={vote === 'down' ? 'secondary' : 'subtle'}
192+
size="xs"
193+
onClick={() => handleVote('down')}
194+
title="Thumbs down"
195+
mr={4}
196+
>
197+
{vote === 'down' ? (
198+
<IconThumbDownFilled size={14} />
199+
) : (
200+
<IconThumbDown size={14} />
201+
)}
202+
</ActionIcon>
203+
<Text size="xs" c="dimmed" className={styles.feedbackLabel}>
204+
Feedback?
205+
</Text>
206+
</span>
207+
<Text
208+
data-testid="feedback-hide"
209+
size="xs"
210+
c="dimmed"
211+
className={styles.feedbackHide}
212+
onClick={() => setHidden(true)}
213+
role="button"
214+
tabIndex={0}
215+
>
216+
Hide
217+
</Text>
218+
</Group>
219+
{state === 'voted' && (
220+
<Box px="lg" pt={4} pb={2}>
221+
<Textarea
222+
data-testid="feedback-comment"
223+
placeholder="Tell us more (optional)"
224+
value={comment}
225+
onChange={e => setComment(e.currentTarget.value)}
226+
minRows={2}
227+
maxRows={4}
228+
autosize
229+
autoFocus
230+
size="xs"
231+
/>
232+
<Button
233+
data-testid="feedback-submit"
234+
variant="primary"
235+
size="compact-xs"
236+
fullWidth
237+
mt={6}
238+
onClick={handleSubmit}
239+
>
240+
Submit
241+
</Button>
242+
</Box>
243+
)}
244+
</>
245+
)}
246+
</Box>
247+
);
248+
};

packages/app/src/components/DBPieChart.tsx

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
BuilderChartConfigWithOptTimestamp,
66
RawSqlConfigWithDateRange,
77
} from '@hyperdx/common-utils/dist/types';
8-
import { Flex } from '@mantine/core';
8+
import { Box, Flex, ScrollArea, Text } from '@mantine/core';
99

1010
import {
1111
buildMVDateRangeIndicator,
@@ -16,7 +16,7 @@ import { useQueriedChartConfig } from '@/hooks/useChartConfig';
1616
import { useMVOptimizationExplanation } from '@/hooks/useMVOptimizationExplanation';
1717
import { useResolvedNumberFormat, useSource } from '@/source';
1818
import type { NumberFormat } from '@/types';
19-
import { getColorProps } from '@/utils';
19+
import { formatNumber, getColorProps, truncateMiddle } from '@/utils';
2020

2121
import ChartContainer from './charts/ChartContainer';
2222
import ChartErrorState, {
@@ -51,6 +51,54 @@ const PieChartTooltip = memo(
5151
},
5252
);
5353

54+
const PieChartLegend = memo(
55+
({
56+
data,
57+
numberFormat,
58+
}: {
59+
data: { label: string; value: number; color: string }[];
60+
numberFormat?: NumberFormat;
61+
}) => {
62+
if (!data.length) return null;
63+
return (
64+
<ScrollArea
65+
data-testid="pie-chart-legend"
66+
type="auto"
67+
style={{ flexShrink: 0, maxWidth: '40%', alignSelf: 'stretch' }}
68+
px="sm"
69+
>
70+
<Flex direction="column" gap={4}>
71+
{data.map(entry => (
72+
<Flex key={entry.label} align="center" gap={6} wrap="nowrap">
73+
<Box
74+
style={{
75+
width: 10,
76+
height: 10,
77+
borderRadius: 2,
78+
backgroundColor: entry.color,
79+
flexShrink: 0,
80+
}}
81+
/>
82+
<Text size="xs" c="dimmed" truncate="end" title={entry.label}>
83+
{truncateMiddle(entry.label, 40)}
84+
</Text>
85+
<Text
86+
size="xs"
87+
c="dimmed"
88+
style={{ flexShrink: 0, marginLeft: 'auto' }}
89+
>
90+
{numberFormat
91+
? formatNumber(entry.value, numberFormat)
92+
: entry.value}
93+
</Text>
94+
</Flex>
95+
))}
96+
</Flex>
97+
</ScrollArea>
98+
);
99+
},
100+
);
101+
54102
export const DBPieChart = ({
55103
config,
56104
title,
@@ -168,7 +216,7 @@ export const DBPieChart = ({
168216
align="center"
169217
justify="center"
170218
h="100%"
171-
style={{ flexGrow: 1 }}
219+
style={{ flexGrow: 1, overflow: 'hidden' }}
172220
>
173221
<ResponsiveContainer
174222
height="100%"
@@ -183,7 +231,6 @@ export const DBPieChart = ({
183231
dataKey="value"
184232
fill="#8884d8"
185233
nameKey="label"
186-
legendType="none"
187234
>
188235
{pieChartData.map(entry => (
189236
<Cell key={entry.label} fill={entry.color} stroke="none" />
@@ -196,6 +243,10 @@ export const DBPieChart = ({
196243
/>
197244
</PieChart>
198245
</ResponsiveContainer>
246+
<PieChartLegend
247+
data={pieChartData}
248+
numberFormat={resolvedNumberFormat}
249+
/>
199250
</Flex>
200251
)}
201252
</ChartContainer>

0 commit comments

Comments
 (0)