Skip to content

Commit a90b33f

Browse files
adrian-codecovnsdeschenesgetsantry[bot]
authored
feat(codecov): Add DateSelector and DatePicker Codecov components (#89117)
Adding components that help choose dates for Codecov components. This is a component inspired from the `DatePageFilter` and `TimeRangeSelector` components. It takes the functionality it needs from those classes and creates custom classes for Codecov purposes. These are capable of saving data to local storage as well as adding URL query params based on date selection. These also only support 1d, 7d and 30d parameters, as per Codecov timeseries backend data. Check it out [here](https://sentry-49loqwkbu.sentry.dev/codecov/tests/) ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --------- Co-authored-by: nicholas-codecov <nicholas.deschenes@sentry.io> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 7fc753c commit a90b33f

File tree

4 files changed

+250
-0
lines changed

4 files changed

+250
-0
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {useEffect} from 'react';
2+
3+
import {updateDateTime} from 'sentry/actionCreators/pageFilters';
4+
import type {DateSelectorProps} from 'sentry/components/codecov/dateSelector';
5+
import {DateSelector} from 'sentry/components/codecov/dateSelector';
6+
import {DesyncedFilterMessage} from 'sentry/components/organizations/pageFilters/desyncedFilter';
7+
import {t} from 'sentry/locale';
8+
import usePageFilters from 'sentry/utils/usePageFilters';
9+
import useRouter from 'sentry/utils/useRouter';
10+
11+
import {isValidCodecovRelativePeriod} from './utils';
12+
13+
const CODECOV_DEFAULT_RELATIVE_PERIOD = '24h';
14+
export const CODECOV_DEFAULT_RELATIVE_PERIODS = {
15+
'24h': t('Last 24 hours'),
16+
'7d': t('Last 7 days'),
17+
'30d': t('Last 30 days'),
18+
};
19+
20+
export interface DatePickerProps
21+
extends Partial<Partial<Omit<DateSelectorProps, 'relative' | 'menuBody'>>> {}
22+
23+
export function DatePicker({
24+
onChange,
25+
menuTitle,
26+
menuWidth,
27+
triggerProps = {},
28+
...selectProps
29+
}: DatePickerProps) {
30+
const router = useRouter();
31+
const {selection, desyncedFilters} = usePageFilters();
32+
const desynced = desyncedFilters.has('datetime');
33+
const period = selection.datetime?.period;
34+
35+
// Adjusts to valid Codecov relative period
36+
useEffect(() => {
37+
if (!isValidCodecovRelativePeriod(period)) {
38+
const newTimePeriod = {period: CODECOV_DEFAULT_RELATIVE_PERIOD};
39+
updateDateTime(newTimePeriod, router, {
40+
save: true,
41+
});
42+
}
43+
}, [period, router]);
44+
45+
return (
46+
<DateSelector
47+
{...selectProps}
48+
relative={period}
49+
desynced={desynced}
50+
onChange={timePeriodUpdate => {
51+
const {relative} = timePeriodUpdate;
52+
const newTimePeriod = {period: relative};
53+
54+
onChange?.(timePeriodUpdate);
55+
updateDateTime(newTimePeriod, router, {
56+
save: true,
57+
});
58+
}}
59+
menuTitle={menuTitle ?? t('Filter Time Range')}
60+
menuWidth={(menuWidth ?? desynced) ? '22em' : undefined}
61+
menuBody={desynced && <DesyncedFilterMessage />}
62+
triggerProps={triggerProps}
63+
/>
64+
);
65+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import {useCallback} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import type {SelectOption, SingleSelectProps} from 'sentry/components/core/compactSelect';
5+
import {CompactSelect} from 'sentry/components/core/compactSelect';
6+
import type {Item} from 'sentry/components/dropdownAutoComplete/types';
7+
import DropdownButton from 'sentry/components/dropdownButton';
8+
import HookOrDefault from 'sentry/components/hookOrDefault';
9+
import {DesyncedFilterIndicator} from 'sentry/components/organizations/pageFilters/desyncedFilter';
10+
import SelectorItems from 'sentry/components/timeRangeSelector/selectorItems';
11+
import {
12+
getArbitraryRelativePeriod,
13+
getSortedRelativePeriods,
14+
} from 'sentry/components/timeRangeSelector/utils';
15+
import {t} from 'sentry/locale';
16+
import {parsePeriodToHours} from 'sentry/utils/duration/parsePeriodToHours';
17+
18+
import {CODECOV_DEFAULT_RELATIVE_PERIODS} from './datePicker';
19+
20+
const SelectorItemsHook = HookOrDefault({
21+
hookName: 'component:header-selector-items',
22+
defaultComponent: SelectorItems,
23+
});
24+
25+
export type ChangeData = {
26+
relative: string | null;
27+
};
28+
29+
export interface DateSelectorProps
30+
extends Omit<
31+
SingleSelectProps<string>,
32+
'disableSearchFilter' | 'onChange' | 'onClose' | 'options' | 'value'
33+
> {
34+
/**
35+
* Whether the current value is out of sync with the stored persistent value.
36+
*/
37+
desynced?: boolean;
38+
/**
39+
* Custom width value for relative compact select
40+
*/
41+
menuWidth?: string;
42+
onChange?: (data: ChangeData) => void;
43+
onClose?: () => void;
44+
/**
45+
* Relative date value
46+
*/
47+
relative?: string | null;
48+
}
49+
50+
export function DateSelector({
51+
relative,
52+
onChange,
53+
onClose,
54+
trigger,
55+
menuWidth,
56+
desynced,
57+
...selectProps
58+
}: DateSelectorProps) {
59+
const getOptions = useCallback((items: Item[]): Array<SelectOption<string>> => {
60+
return items.map((item: Item): SelectOption<string> => {
61+
return {
62+
value: item.value,
63+
label: <OptionLabel>{item.label}</OptionLabel>,
64+
textValue: item.searchKey,
65+
};
66+
});
67+
}, []);
68+
69+
const handleChange = useCallback<NonNullable<SingleSelectProps<string>['onChange']>>(
70+
option => {
71+
onChange?.({relative: option.value});
72+
},
73+
[onChange]
74+
);
75+
76+
// Currently selected relative period
77+
const arbitraryRelativePeriods = getArbitraryRelativePeriod(relative);
78+
// Periods from default relative periods object
79+
const restrictedDefaultPeriods = Object.fromEntries(
80+
Object.entries(CODECOV_DEFAULT_RELATIVE_PERIODS).filter(([period]) =>
81+
parsePeriodToHours(period)
82+
)
83+
);
84+
const defaultRelativePeriods = {
85+
...restrictedDefaultPeriods,
86+
...arbitraryRelativePeriods,
87+
};
88+
89+
return (
90+
<SelectorItemsHook
91+
shouldShowRelative
92+
relativePeriods={getSortedRelativePeriods(defaultRelativePeriods)}
93+
handleSelectRelative={value => handleChange({value})}
94+
>
95+
{items => (
96+
<CompactSelect
97+
{...selectProps}
98+
disableSearchFilter
99+
options={getOptions(items)}
100+
value={relative ?? ''}
101+
onChange={handleChange}
102+
menuWidth={menuWidth ?? '16rem'}
103+
onClose={() => {
104+
onClose?.();
105+
}}
106+
trigger={
107+
trigger ??
108+
((triggerProps, isOpen) => {
109+
const defaultLabel = items.some(item => item.value === relative)
110+
? relative?.toUpperCase()
111+
: t('Invalid Period');
112+
113+
return (
114+
<DropdownButton
115+
isOpen={isOpen}
116+
size={selectProps.size}
117+
data-test-id="page-filter-codecov-time-selector"
118+
{...triggerProps}
119+
{...selectProps.triggerProps}
120+
>
121+
<TriggerLabelWrap>
122+
<TriggerLabel>
123+
{selectProps.triggerLabel ?? defaultLabel}
124+
</TriggerLabel>
125+
{desynced && <DesyncedFilterIndicator />}
126+
</TriggerLabelWrap>
127+
</DropdownButton>
128+
);
129+
})
130+
}
131+
/>
132+
)}
133+
</SelectorItemsHook>
134+
);
135+
}
136+
137+
const TriggerLabelWrap = styled('span')`
138+
position: relative;
139+
min-width: 0;
140+
`;
141+
142+
const TriggerLabel = styled('span')`
143+
${p => p.theme.overflowEllipsis}
144+
width: auto;
145+
`;
146+
147+
const OptionLabel = styled('span')`
148+
div {
149+
margin: 0;
150+
}
151+
`;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {CODECOV_DEFAULT_RELATIVE_PERIODS} from './datePicker';
2+
3+
/**
4+
* Determines if a period is valid for a Codecov DatePicker component. A period is invalid if
5+
* it is null or if it doesn't belong to the list of Codecov default relative periods.
6+
*/
7+
export function isValidCodecovRelativePeriod(period: string | null): boolean {
8+
if (period === null) {
9+
return false;
10+
}
11+
12+
if (!Object.hasOwn(CODECOV_DEFAULT_RELATIVE_PERIODS, period)) {
13+
return false;
14+
}
15+
16+
return true;
17+
}

static/app/views/codecov/tests/tests.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
import styled from '@emotion/styled';
22

3+
import {DatePicker} from 'sentry/components/codecov/datePicker';
4+
import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
5+
import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
36
import {space} from 'sentry/styles/space';
47

8+
export const DEFAULT_CODECOV_DATETIME_SELECTION = {
9+
start: null,
10+
end: null,
11+
utc: false,
12+
period: '24h',
13+
};
14+
515
export default function TestsPage() {
616
return (
717
<LayoutGap>
818
<p>Test Analytics</p>
19+
<PageFiltersContainer
20+
defaultSelection={{datetime: DEFAULT_CODECOV_DATETIME_SELECTION}}
21+
>
22+
<PageFilterBar condensed>
23+
<DatePicker />
24+
</PageFilterBar>
25+
</PageFiltersContainer>
926
</LayoutGap>
1027
);
1128
}

0 commit comments

Comments
 (0)