mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
672 lines
20 KiB
TypeScript
672 lines
20 KiB
TypeScript
import {Fragment, useCallback, useMemo, useRef} from 'react';
|
|
import type {Theme} from '@emotion/react';
|
|
import {css} from '@emotion/react';
|
|
import styled from '@emotion/styled';
|
|
import type {LocationDescriptor} from 'history';
|
|
|
|
import AssigneeSelector from 'sentry/components/assigneeSelector';
|
|
import GuideAnchor from 'sentry/components/assistant/guideAnchor';
|
|
import Checkbox from 'sentry/components/checkbox';
|
|
import Count from 'sentry/components/count';
|
|
import EventOrGroupExtraDetails from 'sentry/components/eventOrGroupExtraDetails';
|
|
import EventOrGroupHeader from 'sentry/components/eventOrGroupHeader';
|
|
import type {GroupListColumn} from 'sentry/components/issues/groupList';
|
|
import Link from 'sentry/components/links/link';
|
|
import PanelItem from 'sentry/components/panels/panelItem';
|
|
import Placeholder from 'sentry/components/placeholder';
|
|
import ProgressBar from 'sentry/components/progressBar';
|
|
import {joinQuery, parseSearch, Token} from 'sentry/components/searchSyntax/parser';
|
|
import GroupChart from 'sentry/components/stream/groupChart';
|
|
import {getRelativeSummary} from 'sentry/components/timeRangeSelector/utils';
|
|
import TimeSince from 'sentry/components/timeSince';
|
|
import {Tooltip} from 'sentry/components/tooltip';
|
|
import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
|
|
import {t} from 'sentry/locale';
|
|
import DemoWalkthroughStore from 'sentry/stores/demoWalkthroughStore';
|
|
import GroupStore from 'sentry/stores/groupStore';
|
|
import SelectedGroupStore from 'sentry/stores/selectedGroupStore';
|
|
import {useLegacyStore} from 'sentry/stores/useLegacyStore';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {
|
|
Group,
|
|
GroupReprocessing,
|
|
InboxDetails,
|
|
NewQuery,
|
|
Organization,
|
|
User,
|
|
} from 'sentry/types';
|
|
import {IssueCategory} from 'sentry/types';
|
|
import {defined, percent} from 'sentry/utils';
|
|
import {trackAnalytics} from 'sentry/utils/analytics';
|
|
import {isDemoWalkthrough} from 'sentry/utils/demoMode';
|
|
import EventView from 'sentry/utils/discover/eventView';
|
|
import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
|
|
import usePageFilters from 'sentry/utils/usePageFilters';
|
|
import withOrganization from 'sentry/utils/withOrganization';
|
|
import type {TimePeriodType} from 'sentry/views/alerts/rules/metric/details/constants';
|
|
import GroupPriority from 'sentry/views/issueDetails/groupPriority';
|
|
import {
|
|
DISCOVER_EXCLUSION_FIELDS,
|
|
getTabs,
|
|
isForReviewQuery,
|
|
} from 'sentry/views/issueList/utils';
|
|
|
|
export const DEFAULT_STREAM_GROUP_STATS_PERIOD = '24h';
|
|
|
|
type Props = {
|
|
id: string;
|
|
organization: Organization;
|
|
canSelect?: boolean;
|
|
customStatsPeriod?: TimePeriodType;
|
|
displayReprocessingLayout?: boolean;
|
|
hasGuideAnchor?: boolean;
|
|
index?: number;
|
|
memberList?: User[];
|
|
narrowGroups?: boolean;
|
|
query?: string;
|
|
queryFilterDescription?: string;
|
|
showLastTriggered?: boolean;
|
|
source?: string;
|
|
statsPeriod?: string;
|
|
useFilteredStats?: boolean;
|
|
useTintRow?: boolean;
|
|
withChart?: boolean;
|
|
withColumns?: GroupListColumn[];
|
|
};
|
|
|
|
function BaseGroupRow({
|
|
id,
|
|
organization,
|
|
customStatsPeriod,
|
|
displayReprocessingLayout,
|
|
hasGuideAnchor,
|
|
index,
|
|
memberList,
|
|
query,
|
|
queryFilterDescription,
|
|
source,
|
|
statsPeriod = DEFAULT_STREAM_GROUP_STATS_PERIOD,
|
|
canSelect = true,
|
|
withChart = true,
|
|
withColumns = ['graph', 'event', 'users', 'priority', 'assignee', 'lastTriggered'],
|
|
useFilteredStats = false,
|
|
useTintRow = true,
|
|
narrowGroups = false,
|
|
showLastTriggered = false,
|
|
}: Props) {
|
|
const groups = useLegacyStore(GroupStore);
|
|
const group = groups.find(item => item.id === id) as Group;
|
|
const issueTypeConfig = getConfigForIssueType(group, group.project);
|
|
|
|
const selectedGroups = useLegacyStore(SelectedGroupStore);
|
|
const isSelected = selectedGroups[id];
|
|
|
|
const {selection} = usePageFilters();
|
|
|
|
const originalInboxState = useRef(group.inbox as InboxDetails | null);
|
|
|
|
const referrer = source ? `${source}-issue-stream` : 'issue-stream';
|
|
|
|
const reviewed =
|
|
// Original state had an inbox reason
|
|
originalInboxState.current?.reason !== undefined &&
|
|
// Updated state has been removed from inbox
|
|
!group.inbox &&
|
|
// Only apply reviewed on the "for review" tab
|
|
isForReviewQuery(query);
|
|
|
|
const {period, start, end} = selection.datetime || {};
|
|
const summary =
|
|
customStatsPeriod?.label.toLowerCase() ??
|
|
(!!start && !!end
|
|
? 'time range'
|
|
: getRelativeSummary(period || DEFAULT_STATS_PERIOD).toLowerCase());
|
|
|
|
const sharedAnalytics = useMemo(() => {
|
|
const tab = getTabs(organization).find(([tabQuery]) => tabQuery === query)?.[1];
|
|
const owners = group.owners ?? [];
|
|
return {
|
|
organization,
|
|
group_id: group.id,
|
|
tab: tab?.analyticsName || 'other',
|
|
was_shown_suggestion: owners.length > 0,
|
|
};
|
|
}, [organization, group.id, group.owners, query]);
|
|
|
|
const trackAssign: React.ComponentProps<typeof AssigneeSelector>['onAssign'] =
|
|
useCallback(
|
|
(type, _assignee, suggestedAssignee) => {
|
|
if (query !== undefined) {
|
|
trackAnalytics('issues_stream.issue_assigned', {
|
|
...sharedAnalytics,
|
|
did_assign_suggestion: !!suggestedAssignee,
|
|
assigned_suggestion_reason: suggestedAssignee?.suggestedReason,
|
|
assigned_type: type,
|
|
});
|
|
}
|
|
},
|
|
[query, sharedAnalytics]
|
|
);
|
|
|
|
const wrapperToggle = useCallback(
|
|
(evt: React.MouseEvent<HTMLDivElement>) => {
|
|
const targetElement = evt.target as Partial<HTMLElement>;
|
|
|
|
// Ignore clicks on links
|
|
if (targetElement?.tagName?.toLowerCase() === 'a') {
|
|
return;
|
|
}
|
|
|
|
// Ignore clicks on the selection checkbox
|
|
if (targetElement?.tagName?.toLowerCase() === 'input') {
|
|
return;
|
|
}
|
|
|
|
let e = targetElement;
|
|
while (e.parentElement) {
|
|
if (e?.tagName?.toLowerCase() === 'a') {
|
|
return;
|
|
}
|
|
e = e.parentElement!;
|
|
}
|
|
|
|
if (evt.shiftKey) {
|
|
SelectedGroupStore.shiftToggleItems(group.id);
|
|
window.getSelection()?.removeAllRanges();
|
|
} else {
|
|
SelectedGroupStore.toggleSelect(group.id);
|
|
}
|
|
},
|
|
[group.id]
|
|
);
|
|
|
|
const checkboxToggle = useCallback(
|
|
(evt: React.ChangeEvent<HTMLInputElement>) => {
|
|
const mouseEvent = evt.nativeEvent as MouseEvent;
|
|
|
|
if (mouseEvent.shiftKey) {
|
|
SelectedGroupStore.shiftToggleItems(group.id);
|
|
} else {
|
|
SelectedGroupStore.toggleSelect(group.id);
|
|
}
|
|
},
|
|
[group.id]
|
|
);
|
|
|
|
const getDiscoverUrl = (isFiltered?: boolean): LocationDescriptor => {
|
|
// when there is no discover feature open events page
|
|
const hasDiscoverQuery = organization.features.includes('discover-basic');
|
|
|
|
const parsedResult = parseSearch(
|
|
isFiltered && typeof query === 'string' ? query : ''
|
|
);
|
|
const filteredTerms = parsedResult?.filter(
|
|
p => !(p.type === Token.FILTER && DISCOVER_EXCLUSION_FIELDS.includes(p.key.text))
|
|
);
|
|
const filteredQuery = joinQuery(filteredTerms, true);
|
|
|
|
const commonQuery = {projects: [Number(group.project.id)]};
|
|
|
|
if (hasDiscoverQuery) {
|
|
const stats = customStatsPeriod ?? (selection.datetime || {});
|
|
|
|
const discoverQuery: NewQuery = {
|
|
...commonQuery,
|
|
id: undefined,
|
|
name: group.title || group.type,
|
|
fields: ['title', 'release', 'environment', 'user', 'timestamp'],
|
|
orderby: '-timestamp',
|
|
query: `issue:${group.shortId}${filteredQuery}`,
|
|
version: 2,
|
|
};
|
|
|
|
if (!!stats.start && !!stats.end) {
|
|
discoverQuery.start = new Date(stats.start).toISOString();
|
|
discoverQuery.end = new Date(stats.end).toISOString();
|
|
if (stats.utc) {
|
|
discoverQuery.utc = true;
|
|
}
|
|
} else {
|
|
discoverQuery.range = stats.period || DEFAULT_STATS_PERIOD;
|
|
}
|
|
|
|
const discoverView = EventView.fromSavedQuery(discoverQuery);
|
|
return discoverView.getResultsViewUrlTarget(organization.slug);
|
|
}
|
|
|
|
return {
|
|
pathname: `/organizations/${organization.slug}/issues/${group.id}/events/`,
|
|
query: {
|
|
referrer,
|
|
stream_index: index,
|
|
...commonQuery,
|
|
query: filteredQuery,
|
|
},
|
|
};
|
|
};
|
|
|
|
const renderReprocessingColumns = () => {
|
|
const {statusDetails, count} = group as GroupReprocessing;
|
|
const {info, pendingEvents} = statusDetails;
|
|
|
|
if (!info) {
|
|
return null;
|
|
}
|
|
|
|
const {totalEvents, dateCreated} = info;
|
|
|
|
const remainingEventsToReprocess = totalEvents - pendingEvents;
|
|
const remainingEventsToReprocessPercent = percent(
|
|
remainingEventsToReprocess,
|
|
totalEvents
|
|
);
|
|
|
|
return (
|
|
<Fragment>
|
|
<StartedColumn>
|
|
<TimeSince date={dateCreated} />
|
|
</StartedColumn>
|
|
<EventsReprocessedColumn>
|
|
{!defined(count) ? (
|
|
<Placeholder height="17px" />
|
|
) : (
|
|
<Fragment>
|
|
<Count value={remainingEventsToReprocess} />
|
|
{'/'}
|
|
<Count value={totalEvents} />
|
|
</Fragment>
|
|
)}
|
|
</EventsReprocessedColumn>
|
|
<ProgressColumn>
|
|
<ProgressBar value={remainingEventsToReprocessPercent} />
|
|
</ProgressColumn>
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
// Use data.filtered to decide on which value to use
|
|
// In case of the query has filters but we avoid showing both sets of filtered/unfiltered stats
|
|
// we use useFilteredStats param passed to Group for deciding
|
|
const primaryCount = group.filtered ? group.filtered.count : group.count;
|
|
const secondaryCount = group.filtered ? group.count : undefined;
|
|
const primaryUserCount = group.filtered ? group.filtered.userCount : group.userCount;
|
|
const secondaryUserCount = group.filtered ? group.userCount : undefined;
|
|
// preview stats
|
|
const lastTriggeredDate = group.lastTriggered;
|
|
|
|
const showSecondaryPoints = Boolean(
|
|
withChart && group && group.filtered && statsPeriod && useFilteredStats
|
|
);
|
|
|
|
const groupCategoryCountTitles: Record<IssueCategory, string> = {
|
|
[IssueCategory.ERROR]: t('Error Events'),
|
|
[IssueCategory.PERFORMANCE]: t('Transaction Events'),
|
|
[IssueCategory.PROFILE]: t('Profile Events'),
|
|
[IssueCategory.CRON]: t('Cron Events'),
|
|
[IssueCategory.REPLAY]: t('Replay Events'),
|
|
};
|
|
|
|
const groupCount = !defined(primaryCount) ? (
|
|
<Placeholder height="18px" />
|
|
) : (
|
|
<GuideAnchor target="dynamic_counts" disabled={!hasGuideAnchor}>
|
|
<Tooltip
|
|
disabled={!useFilteredStats}
|
|
isHoverable
|
|
title={
|
|
<CountTooltipContent>
|
|
<h4>{groupCategoryCountTitles[group.issueCategory]}</h4>
|
|
{group.filtered && (
|
|
<Fragment>
|
|
<div>{queryFilterDescription ?? t('Matching filters')}</div>
|
|
<Link to={getDiscoverUrl(true)}>
|
|
<Count value={group.filtered?.count} />
|
|
</Link>
|
|
</Fragment>
|
|
)}
|
|
<Fragment>
|
|
<div>{t('Total in %s', summary)}</div>
|
|
<Link to={getDiscoverUrl()}>
|
|
<Count value={group.count} />
|
|
</Link>
|
|
</Fragment>
|
|
{group.lifetime && (
|
|
<Fragment>
|
|
<div>{t('Since issue began')}</div>
|
|
<Count value={group.lifetime.count} />
|
|
</Fragment>
|
|
)}
|
|
</CountTooltipContent>
|
|
}
|
|
>
|
|
<PrimaryCount value={primaryCount} />
|
|
{secondaryCount !== undefined && useFilteredStats && (
|
|
<SecondaryCount value={secondaryCount} />
|
|
)}
|
|
</Tooltip>
|
|
</GuideAnchor>
|
|
);
|
|
|
|
const groupUsersCount = !defined(primaryUserCount) ? (
|
|
<Placeholder height="18px" />
|
|
) : (
|
|
<Tooltip
|
|
isHoverable
|
|
disabled={!usePageFilters}
|
|
title={
|
|
<CountTooltipContent>
|
|
<h4>{t('Affected Users')}</h4>
|
|
{group.filtered && (
|
|
<Fragment>
|
|
<div>{queryFilterDescription ?? t('Matching filters')}</div>
|
|
<Link to={getDiscoverUrl(true)}>
|
|
<Count value={group.filtered?.userCount} />
|
|
</Link>
|
|
</Fragment>
|
|
)}
|
|
<Fragment>
|
|
<div>{t('Total in %s', summary)}</div>
|
|
<Link to={getDiscoverUrl()}>
|
|
<Count value={group.userCount} />
|
|
</Link>
|
|
</Fragment>
|
|
{group.lifetime && (
|
|
<Fragment>
|
|
<div>{t('Since issue began')}</div>
|
|
<Count value={group.lifetime.userCount} />
|
|
</Fragment>
|
|
)}
|
|
</CountTooltipContent>
|
|
}
|
|
>
|
|
<PrimaryCount value={primaryUserCount} />
|
|
{secondaryUserCount !== undefined && useFilteredStats && (
|
|
<SecondaryCount dark value={secondaryUserCount} />
|
|
)}
|
|
</Tooltip>
|
|
);
|
|
|
|
const lastTriggered = !defined(lastTriggeredDate) ? (
|
|
<Placeholder height="18px" />
|
|
) : (
|
|
<TimeSince
|
|
tooltipPrefix={t('Last Triggered')}
|
|
date={lastTriggeredDate}
|
|
suffix={t('ago')}
|
|
unitStyle="short"
|
|
/>
|
|
);
|
|
|
|
const issueStreamAnchor = isDemoWalkthrough() ? (
|
|
<GuideAnchor target="issue_stream" disabled={!DemoWalkthroughStore.get('issue')} />
|
|
) : (
|
|
<GuideAnchor target="issue_stream" />
|
|
);
|
|
|
|
return (
|
|
<Wrapper
|
|
data-test-id="group"
|
|
data-test-reviewed={reviewed}
|
|
onClick={displayReprocessingLayout || !canSelect ? undefined : wrapperToggle}
|
|
reviewed={reviewed}
|
|
useTintRow={useTintRow ?? true}
|
|
>
|
|
{canSelect && (
|
|
<GroupCheckBoxWrapper>
|
|
<Checkbox
|
|
id={group.id}
|
|
aria-label={t('Select Issue')}
|
|
checked={isSelected}
|
|
disabled={!!displayReprocessingLayout}
|
|
onChange={checkboxToggle}
|
|
/>
|
|
</GroupCheckBoxWrapper>
|
|
)}
|
|
<GroupSummary canSelect={canSelect}>
|
|
<EventOrGroupHeader
|
|
index={index}
|
|
organization={organization}
|
|
data={group}
|
|
query={query}
|
|
size="normal"
|
|
source={referrer}
|
|
/>
|
|
<EventOrGroupExtraDetails data={group} />
|
|
</GroupSummary>
|
|
{hasGuideAnchor && issueStreamAnchor}
|
|
|
|
{withChart && !displayReprocessingLayout && issueTypeConfig.stats.enabled && (
|
|
<ChartWrapper narrowGroups={narrowGroups}>
|
|
<GroupChart
|
|
statsPeriod={statsPeriod!}
|
|
data={group}
|
|
showSecondaryPoints={showSecondaryPoints}
|
|
showMarkLine
|
|
/>
|
|
</ChartWrapper>
|
|
)}
|
|
{displayReprocessingLayout ? (
|
|
renderReprocessingColumns()
|
|
) : (
|
|
<Fragment>
|
|
{withColumns.includes('event') && issueTypeConfig.stats.enabled && (
|
|
<EventCountsWrapper>{groupCount}</EventCountsWrapper>
|
|
)}
|
|
{withColumns.includes('users') && issueTypeConfig.stats.enabled && (
|
|
<EventCountsWrapper>{groupUsersCount}</EventCountsWrapper>
|
|
)}
|
|
{organization.features.includes('issue-priority-ui') &&
|
|
withColumns.includes('priority') ? (
|
|
<PriorityWrapper narrowGroups={narrowGroups}>
|
|
{group.priority ? <GroupPriority group={group} /> : null}
|
|
</PriorityWrapper>
|
|
) : null}
|
|
{withColumns.includes('assignee') && (
|
|
<AssigneeWrapper narrowGroups={narrowGroups}>
|
|
<AssigneeSelector
|
|
id={group.id}
|
|
memberList={memberList}
|
|
onAssign={trackAssign}
|
|
/>
|
|
</AssigneeWrapper>
|
|
)}
|
|
{showLastTriggered && <EventCountsWrapper>{lastTriggered}</EventCountsWrapper>}
|
|
</Fragment>
|
|
)}
|
|
</Wrapper>
|
|
);
|
|
}
|
|
|
|
const StreamGroup = withOrganization(BaseGroupRow);
|
|
|
|
export default StreamGroup;
|
|
|
|
// Position for wrapper is relative for overlay actions
|
|
const Wrapper = styled(PanelItem)<{
|
|
reviewed: boolean;
|
|
useTintRow: boolean;
|
|
}>`
|
|
position: relative;
|
|
padding: ${space(1.5)} 0;
|
|
line-height: 1.1;
|
|
|
|
${p =>
|
|
p.useTintRow &&
|
|
p.reviewed &&
|
|
css`
|
|
animation: tintRow 0.2s linear forwards;
|
|
position: relative;
|
|
|
|
/*
|
|
* A mask that fills the entire row and makes the text opaque. Doing this because
|
|
* opacity adds a stacking context in CSS so we need to apply it to another element.
|
|
*/
|
|
&:after {
|
|
content: '';
|
|
pointer-events: none;
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: ${p.theme.bodyBackground};
|
|
opacity: 0.4;
|
|
}
|
|
|
|
@keyframes tintRow {
|
|
0% {
|
|
background-color: ${p.theme.bodyBackground};
|
|
}
|
|
100% {
|
|
background-color: ${p.theme.backgroundSecondary};
|
|
}
|
|
}
|
|
`};
|
|
`;
|
|
|
|
const GroupSummary = styled('div')<{canSelect: boolean}>`
|
|
overflow: hidden;
|
|
margin-left: ${p => space(p.canSelect ? 1 : 2)};
|
|
margin-right: ${space(1)};
|
|
flex: 1;
|
|
width: 66.66%;
|
|
|
|
@media (min-width: ${p => p.theme.breakpoints.medium}) {
|
|
width: 50%;
|
|
}
|
|
`;
|
|
|
|
const GroupCheckBoxWrapper = styled('div')`
|
|
margin-left: ${space(2)};
|
|
align-self: flex-start;
|
|
height: 15px;
|
|
display: flex;
|
|
align-items: center;
|
|
`;
|
|
|
|
const primaryStatStyle = (theme: Theme) => css`
|
|
font-size: ${theme.fontSizeLarge};
|
|
font-variant-numeric: tabular-nums;
|
|
`;
|
|
|
|
const PrimaryCount = styled(Count)`
|
|
${p => primaryStatStyle(p.theme)};
|
|
`;
|
|
|
|
const secondaryStatStyle = (theme: Theme) => css`
|
|
font-size: ${theme.fontSizeLarge};
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
:before {
|
|
content: '/';
|
|
padding-left: ${space(0.25)};
|
|
padding-right: 2px;
|
|
color: ${theme.gray300};
|
|
}
|
|
`;
|
|
|
|
const SecondaryCount = styled(({value, ...p}) => <Count {...p} value={value} />)`
|
|
${p => secondaryStatStyle(p.theme)}
|
|
`;
|
|
|
|
const CountTooltipContent = styled('div')`
|
|
display: grid;
|
|
grid-template-columns: 1fr max-content;
|
|
gap: ${space(1)} ${space(3)};
|
|
text-align: left;
|
|
font-size: ${p => p.theme.fontSizeMedium};
|
|
align-items: center;
|
|
|
|
h4 {
|
|
color: ${p => p.theme.gray300};
|
|
font-size: ${p => p.theme.fontSizeExtraSmall};
|
|
text-transform: uppercase;
|
|
grid-column: 1 / -1;
|
|
margin-bottom: ${space(0.25)};
|
|
}
|
|
`;
|
|
|
|
const ChartWrapper = styled('div')<{narrowGroups: boolean}>`
|
|
width: 200px;
|
|
align-self: center;
|
|
|
|
@media (max-width: ${p => (p.narrowGroups ? '1600px' : p.theme.breakpoints.xlarge)}) {
|
|
display: none;
|
|
}
|
|
`;
|
|
|
|
const EventCountsWrapper = styled('div')`
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
align-self: center;
|
|
width: 60px;
|
|
margin: 0 ${space(2)};
|
|
|
|
@media (min-width: ${p => p.theme.breakpoints.xlarge}) {
|
|
width: 80px;
|
|
}
|
|
`;
|
|
|
|
const PriorityWrapper = styled('div')<{narrowGroups: boolean}>`
|
|
width: 85px;
|
|
margin: 0 ${space(2)};
|
|
align-self: center;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
|
|
@media (max-width: ${p =>
|
|
p.narrowGroups ? p.theme.breakpoints.large : p.theme.breakpoints.medium}) {
|
|
display: none;
|
|
}
|
|
`;
|
|
|
|
const AssigneeWrapper = styled('div')<{narrowGroups: boolean}>`
|
|
width: 60px;
|
|
margin: 0 ${space(2)};
|
|
align-self: center;
|
|
|
|
@media (max-width: ${p =>
|
|
p.narrowGroups ? p.theme.breakpoints.large : p.theme.breakpoints.medium}) {
|
|
display: none;
|
|
}
|
|
`;
|
|
|
|
// Reprocessing
|
|
const StartedColumn = styled('div')`
|
|
align-self: center;
|
|
margin: 0 ${space(2)};
|
|
color: ${p => p.theme.gray500};
|
|
${p => p.theme.overflowEllipsis};
|
|
width: 85px;
|
|
|
|
@media (min-width: ${p => p.theme.breakpoints.small}) {
|
|
display: block;
|
|
width: 140px;
|
|
}
|
|
`;
|
|
|
|
const EventsReprocessedColumn = styled('div')`
|
|
align-self: center;
|
|
margin: 0 ${space(2)};
|
|
color: ${p => p.theme.gray500};
|
|
${p => p.theme.overflowEllipsis};
|
|
width: 75px;
|
|
|
|
@media (min-width: ${p => p.theme.breakpoints.small}) {
|
|
width: 140px;
|
|
}
|
|
`;
|
|
|
|
const ProgressColumn = styled('div')`
|
|
margin: 0 ${space(2)};
|
|
align-self: center;
|
|
display: none;
|
|
|
|
@media (min-width: ${p => p.theme.breakpoints.small}) {
|
|
display: block;
|
|
width: 160px;
|
|
}
|
|
`;
|