Skip to content

Commit f1660e8

Browse files
authored
feat(tracemetrics): Add panel switcher for orientation and hiding tables (#102496)
Adds a panel switcher that can be used to change the table orientation from the right side or below the chart. If the screen is sufficiently small, we'll also force the orientation into a stacked orientation to save space. Also adds a button that can be used to hide the table. This PR does not currently commit the changes to the URL but I will follow up with a PR to add that.
1 parent d4bd618 commit f1660e8

File tree

13 files changed

+476
-110
lines changed

13 files changed

+476
-110
lines changed

static/app/views/dashboards/widgets/widget/widget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ const VisualizationWrapper = styled('div')<{noPadding?: boolean}>`
181181
padding: ${p => (p.noPadding ? 0 : `0 ${X_GUTTER} ${Y_GUTTER} ${X_GUTTER}`)};
182182
`;
183183

184-
const FooterWrapper = styled('div')<{noPadding?: boolean}>`
184+
export const FooterWrapper = styled('div')<{noPadding?: boolean}>`
185185
margin: 0;
186186
border-top: 1px solid ${p => p.theme.border};
187187
padding: ${p => (p.noPadding ? 0 : `${space(1)} ${X_GUTTER} ${space(1)} ${X_GUTTER}`)};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {useState} from 'react';
2+
3+
import {useBreakpoints} from 'sentry/utils/useBreakpoints';
4+
5+
export type TableOrientation = 'right' | 'bottom';
6+
7+
export function useTableOrientationControl(): {
8+
canChangeOrientation: boolean;
9+
orientation: TableOrientation;
10+
setOrientation: (orientation: TableOrientation) => void;
11+
userPreferenceOrientation: TableOrientation;
12+
} {
13+
const breakpoints = useBreakpoints();
14+
const [userPreference, setUserPreference] = useState<TableOrientation>('right');
15+
16+
// Derive the actual orientation based on screen size
17+
const effectiveOrientation = breakpoints.md ? userPreference : 'bottom';
18+
const canChangeOrientation = breakpoints.md;
19+
20+
return {
21+
orientation: effectiveOrientation,
22+
userPreferenceOrientation: userPreference,
23+
setOrientation: setUserPreference,
24+
canChangeOrientation,
25+
};
26+
}

static/app/views/explore/metrics/metricGraph.tsx renamed to static/app/views/explore/metrics/metricGraph/index.tsx

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {ChartVisualization} from 'sentry/views/explore/components/chart/chartVis
1111
import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval';
1212
import {TOP_EVENTS_LIMIT} from 'sentry/views/explore/hooks/useTopEvents';
1313
import {ConfidenceFooter} from 'sentry/views/explore/metrics/confidenceFooter';
14+
import type {TableOrientation} from 'sentry/views/explore/metrics/hooks/useOrientationControl';
1415
import {
1516
useMetricLabel,
1617
useMetricVisualize,
@@ -29,12 +30,26 @@ import {
2930
import {ChartType} from 'sentry/views/insights/common/components/chart';
3031
import type {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSortedTimeSeries';
3132

33+
import {WidgetWrapper} from './styles';
34+
35+
const MINIMIZED_GRAPH_HEIGHT = 50;
36+
const STACKED_GRAPH_HEIGHT = 362;
37+
3238
interface MetricsGraphProps {
39+
orientation: TableOrientation;
3340
queryIndex: number;
3441
timeseriesResult: ReturnType<typeof useSortedTimeSeries>;
42+
additionalActions?: React.ReactNode;
43+
infoContentHidden?: boolean;
3544
}
3645

37-
export function MetricsGraph({timeseriesResult, queryIndex}: MetricsGraphProps) {
46+
export function MetricsGraph({
47+
timeseriesResult,
48+
queryIndex,
49+
orientation,
50+
additionalActions,
51+
infoContentHidden,
52+
}: MetricsGraphProps) {
3853
const visualize = useMetricVisualize();
3954
const setVisualize = useSetMetricVisualize();
4055

@@ -48,6 +63,9 @@ export function MetricsGraph({timeseriesResult, queryIndex}: MetricsGraphProps)
4863
timeseriesResult={timeseriesResult}
4964
onChartTypeChange={handleChartTypeChange}
5065
queryIndex={queryIndex}
66+
orientation={orientation}
67+
additionalActions={additionalActions}
68+
infoContentHidden={infoContentHidden}
5169
/>
5270
);
5371
}
@@ -58,7 +76,15 @@ interface GraphProps extends MetricsGraphProps {
5876
visualize: ReturnType<typeof useMetricVisualize>;
5977
}
6078

61-
function Graph({onChartTypeChange, timeseriesResult, queryIndex, visualize}: GraphProps) {
79+
function Graph({
80+
onChartTypeChange,
81+
timeseriesResult,
82+
queryIndex,
83+
orientation,
84+
visualize,
85+
infoContentHidden,
86+
additionalActions,
87+
}: GraphProps) {
6288
const aggregate = visualize.yAxis;
6389
const topEventsLimit = useQueryParamsTopEventsLimit();
6490
const metricLabel = useMetricLabel();
@@ -126,26 +152,35 @@ function Graph({onChartTypeChange, timeseriesResult, queryIndex, visualize}: Gra
126152
options={intervalOptions}
127153
/>
128154
</Tooltip>
155+
{additionalActions}
129156
</Fragment>
130157
);
131158

132159
return (
133-
<Widget
134-
Title={Title}
135-
Actions={Actions}
136-
Visualization={visualize.visible && <ChartVisualization chartInfo={chartInfo} />}
137-
Footer={
138-
visualize.visible && (
139-
<ConfidenceFooter
140-
chartInfo={chartInfo}
141-
isLoading={timeseriesResult.isFetching}
142-
hasUserQuery={!!userQuery}
143-
/>
144-
)
145-
}
146-
height={visualize.visible ? undefined : 0}
147-
revealActions="always"
148-
borderless
149-
/>
160+
<WidgetWrapper hideFooterBorder={orientation === 'bottom'}>
161+
<Widget
162+
Title={Title}
163+
Actions={Actions}
164+
Visualization={visualize.visible && <ChartVisualization chartInfo={chartInfo} />}
165+
Footer={
166+
visualize.visible && (
167+
<ConfidenceFooter
168+
chartInfo={chartInfo}
169+
isLoading={timeseriesResult.isFetching}
170+
hasUserQuery={!!userQuery}
171+
/>
172+
)
173+
}
174+
height={
175+
visualize.visible
176+
? orientation === 'bottom' || infoContentHidden
177+
? STACKED_GRAPH_HEIGHT
178+
: undefined
179+
: MINIMIZED_GRAPH_HEIGHT
180+
}
181+
revealActions="always"
182+
borderless
183+
/>
184+
</WidgetWrapper>
150185
);
151186
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {css} from '@emotion/react';
2+
import styled from '@emotion/styled';
3+
4+
import {FooterWrapper} from 'sentry/views/dashboards/widgets/widget/widget';
5+
6+
// hideFooterBorder is used to hide the top border of the footer when
7+
// the aggregates/samples tables are on the bottom. This is so the footer
8+
// maintains its visual relationship with the graph and not the tables.
9+
export const WidgetWrapper = styled('div')<{hideFooterBorder?: boolean}>`
10+
height: 100%;
11+
${p =>
12+
p.hideFooterBorder &&
13+
css`
14+
${FooterWrapper} {
15+
border-top: none;
16+
}
17+
`}
18+
`;

static/app/views/explore/metrics/metricInfoTabs/index.tsx

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import styled from '@emotion/styled';
1+
import {Flex} from '@sentry/scraps/layout';
22

33
import {TabList, TabPanels, TabStateProvider} from 'sentry/components/core/tabs';
44
import {t} from 'sentry/locale';
5+
import type {TableOrientation} from 'sentry/views/explore/metrics/hooks/useOrientationControl';
56
import {AggregatesTab} from 'sentry/views/explore/metrics/metricInfoTabs/aggregatesTab';
6-
import {TabListWrapper} from 'sentry/views/explore/metrics/metricInfoTabs/metricInfoTabStyles';
7+
import {
8+
BodyContainer,
9+
StyledTabPanels,
10+
TabListWrapper,
11+
} from 'sentry/views/explore/metrics/metricInfoTabs/metricInfoTabStyles';
712
import {SamplesTab} from 'sentry/views/explore/metrics/metricInfoTabs/samplesTab';
813
import type {TraceMetric} from 'sentry/views/explore/metrics/metricQuery';
914
import {useMetricVisualize} from 'sentry/views/explore/metrics/metricsQueryParams';
@@ -14,10 +19,18 @@ import {
1419
import {Mode} from 'sentry/views/explore/queryParams/mode';
1520

1621
interface MetricInfoTabsProps {
22+
orientation: TableOrientation;
1723
traceMetric: TraceMetric;
24+
additionalActions?: React.ReactNode;
25+
contentsHidden?: boolean;
1826
}
1927

20-
export default function MetricInfoTabs({traceMetric}: MetricInfoTabsProps) {
28+
export default function MetricInfoTabs({
29+
traceMetric,
30+
additionalActions,
31+
contentsHidden,
32+
orientation,
33+
}: MetricInfoTabsProps) {
2134
const visualize = useMetricVisualize();
2235
const queryParamsMode = useQueryParamsMode();
2336
const setAggregatesMode = useSetQueryParamsMode();
@@ -29,14 +42,22 @@ export default function MetricInfoTabs({traceMetric}: MetricInfoTabsProps) {
2942
}}
3043
size="xs"
3144
>
32-
<TabListWrapper>
33-
<TabList>
34-
<TabList.Item key={Mode.AGGREGATE}>{t('Aggregates')}</TabList.Item>
35-
<TabList.Item key={Mode.SAMPLES}>{t('Samples')}</TabList.Item>
36-
</TabList>
37-
</TabListWrapper>
38-
39-
{visualize.visible && (
45+
{(orientation === 'right' || visualize.visible) && (
46+
<Flex direction="row" justify="between" align="center" paddingRight="xl">
47+
<TabListWrapper orientation={orientation}>
48+
<TabList>
49+
<TabList.Item key={Mode.AGGREGATE} disabled={contentsHidden}>
50+
{t('Aggregates')}
51+
</TabList.Item>
52+
<TabList.Item key={Mode.SAMPLES} disabled={contentsHidden}>
53+
{t('Samples')}
54+
</TabList.Item>
55+
</TabList>
56+
</TabListWrapper>
57+
{additionalActions}
58+
</Flex>
59+
)}
60+
{visualize.visible && !contentsHidden && (
4061
<BodyContainer>
4162
<StyledTabPanels>
4263
<TabPanels.Item key={Mode.AGGREGATE}>
@@ -51,14 +72,3 @@ export default function MetricInfoTabs({traceMetric}: MetricInfoTabsProps) {
5172
</TabStateProvider>
5273
);
5374
}
54-
55-
const BodyContainer = styled('div')`
56-
padding: ${p => p.theme.space.md};
57-
padding-top: 0;
58-
height: 320px;
59-
container-type: inline-size;
60-
`;
61-
62-
const StyledTabPanels = styled(TabPanels)`
63-
overflow: auto;
64-
`;

static/app/views/explore/metrics/metricInfoTabs/metricInfoTabStyles.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
1+
import {css} from '@emotion/react';
12
import styled from '@emotion/styled';
23

4+
import {TabPanels} from '@sentry/scraps/tabs';
5+
36
import {SimpleTable} from 'sentry/components/tables/simpleTable';
47
import {TopResultsIndicator} from 'sentry/views/discover/table/topResultsIndicator';
58
import {DetailsWrapper} from 'sentry/views/explore/logs/styles';
9+
import type {TableOrientation} from 'sentry/views/explore/metrics/hooks/useOrientationControl';
610
import {StyledPanel} from 'sentry/views/explore/tables/tracesTable/styles';
711

8-
export const TabListWrapper = styled('div')`
9-
padding-top: ${p => p.theme.space.sm};
12+
export const TabListWrapper = styled('div')<{orientation: TableOrientation}>`
13+
padding-top: 10px;
14+
width: 100%;
15+
16+
${p =>
17+
p.orientation === 'bottom' &&
18+
css`
19+
padding-top: 0;
20+
padding-bottom: 1px;
21+
`}
1022
`;
1123

1224
export const StyledTopResultsIndicator = styled(TopResultsIndicator)``;
@@ -105,3 +117,14 @@ export const NumericSimpleTableHeaderCell = styled(StyledSimpleTableHeaderCell)`
105117
export const NumericSimpleTableRowCell = styled(StyledSimpleTableRowCell)`
106118
justify-content: flex-end;
107119
`;
120+
121+
export const BodyContainer = styled('div')`
122+
padding: ${p => p.theme.space.md};
123+
padding-top: 0;
124+
height: 320px;
125+
container-type: inline-size;
126+
`;
127+
128+
export const StyledTabPanels = styled(TabPanels)`
129+
overflow: auto;
130+
`;

static/app/views/explore/metrics/metricPanel.tsx

Lines changed: 0 additions & 66 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {Button} from '@sentry/scraps/button';
2+
3+
import {t} from 'sentry/locale';
4+
import type {TableOrientation} from 'sentry/views/explore/metrics/hooks/useOrientationControl';
5+
import {HIDE_BUTTONS_BY_ORIENTATION} from 'sentry/views/explore/metrics/metricPanel/utils';
6+
7+
interface HideContentButtonProps {
8+
infoContentHidden: boolean;
9+
onToggle: () => void;
10+
orientation: TableOrientation;
11+
}
12+
13+
export function HideContentButton({
14+
orientation,
15+
infoContentHidden,
16+
onToggle,
17+
}: HideContentButtonProps) {
18+
const {IconShow, IconHide} = HIDE_BUTTONS_BY_ORIENTATION[orientation];
19+
const Icon = infoContentHidden ? IconShow : IconHide;
20+
21+
return (
22+
<Button
23+
size="zero"
24+
borderless
25+
aria-label={infoContentHidden ? t('Show Table') : t('Hide Table')}
26+
icon={<Icon />}
27+
onClick={onToggle}
28+
title={infoContentHidden ? t('Show Table') : t('Hide Table')}
29+
/>
30+
);
31+
}

0 commit comments

Comments
 (0)