@@ -87,7 +72,8 @@ export default Storybook.story('Breadcrumbs', story => {
],
[
{
- label: 'longlonglonglonglonglonglonglonglonglonglonglonglonglonglong',
+ label:
+ 'A Very Long Project Name Here That Will Be Truncated Because It Is Too Long',
to: '/org/',
},
{label: 'Very Long Project Name Here', to: '/project/'},
diff --git a/static/app/components/breadcrumbs.tsx b/static/app/components/breadcrumbs.tsx
index 1f31d0f087eb84..8458571338a012 100644
--- a/static/app/components/breadcrumbs.tsx
+++ b/static/app/components/breadcrumbs.tsx
@@ -1,31 +1,19 @@
import {Fragment} from 'react';
-import type {Theme} from '@emotion/react';
-import {css} from '@emotion/react';
-import styled from '@emotion/styled';
-import {Chevron} from 'sentry/components/chevron';
+import {Container, Flex} from '@sentry/scraps/layout';
+import {Text} from '@sentry/scraps/text';
+
import type {LinkProps} from 'sentry/components/core/link';
import {Link} from 'sentry/components/core/link';
import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
-import {space} from 'sentry/styles/space';
-
-const BreadcrumbList = styled('nav')`
- display: flex;
- align-items: center;
- padding: ${space(1)} 0;
-`;
+import {IconChevron} from 'sentry/icons';
+import {trackAnalytics} from 'sentry/utils/analytics';
export interface Crumb {
/**
* Label of the crumb
*/
- label: React.ReactNode;
-
- /**
- * Component will try to come up with unique key, but you can provide your own
- * (used when mapping over crumbs)
- */
- key?: string;
+ label: NonNullable;
/**
* It will keep the page filter values (projects, environments, time) in the
@@ -39,103 +27,98 @@ export interface Crumb {
to?: LinkProps['to'] | null;
}
-interface Props extends React.HTMLAttributes {
- /**
- * Array of crumbs that will be rendered
- */
+interface BreadcrumbsProps extends React.HTMLAttributes {
crumbs: Crumb[];
-
- /**
- * As a general rule of thumb we don't want the last item to be link as it most likely
- * points to the same page we are currently on. This is by default false, so that
- * people don't have to check if crumb is last in the array and then manually
- * assign `to: null/undefined` when passing props to this component.
- */
- linkLastItem?: boolean;
}
/**
* Page breadcrumbs used for navigation, not to be confused with sentry's event breadcrumbs
*/
-export function Breadcrumbs({crumbs, linkLastItem = false, ...props}: Props) {
+export function Breadcrumbs({crumbs, ...props}: BreadcrumbsProps) {
if (crumbs.length === 0) {
return null;
}
- if (!linkLastItem) {
- crumbs[crumbs.length - 1]!.to = null;
- }
-
return (
-
+
{crumbs.map((crumb, index) => {
- const {label, to, preservePageFilters, key} = crumb;
- const labelKey = typeof label === 'string' ? label : '';
- const mapKey =
- key ?? (typeof to === 'string' ? `${labelKey}${to}` : `${labelKey}${index}`);
-
return (
-
- {to ? (
-
- {label}
-
- ) : (
- {label}
- )}
-
- {index < crumbs.length - 1 && }
+
+
+ {index < crumbs.length - 1 ? (
+
+
+
+ ) : null}
);
})}
-
+
);
}
-const getBreadcrumbListItemStyles = (p: {theme: Theme}) => css`
- ${p.theme.overflowEllipsis}
- color: ${p.theme.subText};
- width: auto;
+interface BreadCrumbItemProps {
+ crumb: Crumb;
+ variant: 'primary' | 'muted';
+}
- &:last-child {
- color: ${p.theme.textColor};
+function BreadCrumbItem(props: BreadCrumbItemProps) {
+ function onBreadcrumbLinkClick() {
+ if (props.crumb.to) {
+ trackAnalytics('breadcrumbs.link.clicked', {organization: null});
+ }
}
-`;
-interface BreadcrumbLinkProps {
- to: LinkProps['to'];
+ return (
+
+ {styleProps => {
+ return props.crumb.to ? (
+
+
+ {props.crumb.label}
+
+
+ ) : (
+
+ {props.crumb.label}
+
+ );
+ }}
+
+ );
+}
+
+interface BreadcrumbLinkProps extends LinkProps {
children?: React.ReactNode;
preservePageFilters?: boolean;
}
-const BreadcrumbLink = styled(
- ({preservePageFilters, to, ...props}: BreadcrumbLinkProps) =>
- preservePageFilters ? (
-
- ) : (
-
- )
-)`
- ${getBreadcrumbListItemStyles}
- max-width: 400px;
-
- &:hover,
- &:active {
- color: ${p => p.theme.subText};
+function BreadcrumbLink(props: BreadcrumbLinkProps) {
+ const {preservePageFilters, ...rest} = props;
+ if (preservePageFilters) {
+ return ;
}
-`;
-const BreadcrumbItem = styled('span')`
- ${getBreadcrumbListItemStyles}
- max-width: 400px;
-`;
-
-const BreadcrumbDividerIcon = styled(Chevron)`
- color: ${p => p.theme.subText};
- margin: 0 ${space(0.5)};
- flex-shrink: 0;
-`;
+ return ;
+}
diff --git a/static/app/components/chevron.tsx b/static/app/components/chevron.tsx
deleted file mode 100644
index 57f5c0afe940d5..00000000000000
--- a/static/app/components/chevron.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import {useTheme} from '@emotion/react';
-import styled from '@emotion/styled';
-import {motion} from 'framer-motion';
-
-interface ChevronProps extends React.SVGAttributes {
- direction?: 'up' | 'right' | 'down' | 'left';
- /**
- * Whether to lighten (by lowering the opacity) the chevron. Useful if the chevron is
- * inside a dropdown trigger button.
- */
- light?: boolean;
- /**
- * The size of the checkbox. Defaults to 'sm'.
- */
- size?: 'large' | 'medium' | 'small';
- weight?: 'regular' | 'medium';
-}
-
-const rubikWeightFactor: Record, number> = {
- regular: 1,
- medium: 1.4,
-};
-
-function getPath(direction: NonNullable) {
- // Base values for a downward chevron
- const base = [
- [3.5, 5.5],
- [7, 9],
- [10.5, 5.5],
- ] as const;
-
- switch (direction) {
- case 'right':
- // Switch X and Y axis (so `base[0][1]` goes before `base[0][1]`)
- return `M${base[0][1]} ${base[0][0]}L${base[1][1]} ${base[1][0]}L${base[2][1]} ${base[2][0]}`;
- case 'left':
- // Switch X and Y axis, then flip X axis (so 14 - …)
- return `M${14 - base[0][1]} ${base[0][0]}L${14 - base[1][1]} ${base[1][0]}L${14 - base[2][1]} ${base[2][0]}`;
- case 'up':
- // Flip Y axis (so 14 - …)
- return `M${base[0][0]} ${14 - base[0][1]}L${base[1][0]} ${14 - base[1][1]}L${base[2][0]} ${14 - base[2][1]}`;
- case 'down':
- default:
- return `M${base[0][0]} ${base[0][1]}L${base[1][0]} ${base[1][1]}L${base[2][0]} ${base[2][1]}`;
- }
-}
-
-function Chevron({
- size = 'medium',
- weight = 'regular',
- direction = 'down',
- light = false,
- ...props
-}: ChevronProps) {
- const theme = useTheme();
- return (
-
-
-
- );
-}
-
-const VariableWeightIcon = styled('svg')<{size: string; weightFactor: number}>`
- width: ${p => p.size};
- height: ${p => p.size};
-
- fill: none;
- stroke: currentColor;
- stroke-linecap: round;
- stroke-linejoin: round;
- stroke-width: calc(${p => p.size} * 0.0875 * ${p => p.weightFactor});
-`;
-
-export {Chevron};
diff --git a/static/app/utils/analytics.tsx b/static/app/utils/analytics.tsx
index f81190492e8e4c..9cf3b07d6c5fa6 100644
--- a/static/app/utils/analytics.tsx
+++ b/static/app/utils/analytics.tsx
@@ -40,6 +40,8 @@ import {
import type {AgentMonitoringEventParameters} from './analytics/agentMonitoringAnalyticsEvents';
import {agentMonitoringEventMap} from './analytics/agentMonitoringAnalyticsEvents';
+import type {BreadcrumbsAnalyticsEventParameters} from './analytics/breadcrumbsAnalyticsEvents';
+import {breadcrumbsAnalyticsEventMap} from './analytics/breadcrumbsAnalyticsEvents';
import type {CoreUIEventParameters} from './analytics/coreuiAnalyticsEvents';
import {coreUIEventMap} from './analytics/coreuiAnalyticsEvents';
import type {DashboardsEventParameters} from './analytics/dashboardsAnalyticsEvents';
@@ -98,6 +100,7 @@ interface EventParameters
extends GrowthEventParameters,
AgentMonitoringEventParameters,
AlertsEventParameters,
+ BreadcrumbsAnalyticsEventParameters,
CoreUIEventParameters,
DashboardsEventParameters,
DiscoverEventParameters,
@@ -135,6 +138,7 @@ interface EventParameters
const allEventMap: Record = {
...agentMonitoringEventMap,
...alertsEventMap,
+ ...breadcrumbsAnalyticsEventMap,
...coreUIEventMap,
...dashboardsEventMap,
...discoverEventMap,
diff --git a/static/app/utils/analytics/breadcrumbsAnalyticsEvents.tsx b/static/app/utils/analytics/breadcrumbsAnalyticsEvents.tsx
new file mode 100644
index 00000000000000..2c271357ac326d
--- /dev/null
+++ b/static/app/utils/analytics/breadcrumbsAnalyticsEvents.tsx
@@ -0,0 +1,12 @@
+export type BreadcrumbsAnalyticsEventParameters = {
+ 'breadcrumbs.link.clicked': {organization: null};
+ 'breadcrumbs.menu.opened': {organization: null};
+};
+
+export const breadcrumbsAnalyticsEventMap: Record<
+ keyof BreadcrumbsAnalyticsEventParameters,
+ string | null
+> = {
+ 'breadcrumbs.link.clicked': 'Breadcrumbs: Link Clicked',
+ 'breadcrumbs.menu.opened': 'Breadcrumbs: Menu Opened',
+};
diff --git a/static/app/views/discover/landing.spec.tsx b/static/app/views/discover/landing.spec.tsx
index 74562fcf2def8b..c05ee114b0b567 100644
--- a/static/app/views/discover/landing.spec.tsx
+++ b/static/app/views/discover/landing.spec.tsx
@@ -116,7 +116,7 @@ describe('Discover > Landing', () => {
);
- expect(await screen.findByText('Discover')).toHaveAttribute(
+ expect(await screen.findByRole('link', {name: 'Discover'})).toHaveAttribute(
'href',
'/organizations/org-slug/explore/discover/homepage/'
);
diff --git a/static/app/views/discover/landing.tsx b/static/app/views/discover/landing.tsx
index ad11f8a7e8e4b4..d7a1c713bdef90 100644
--- a/static/app/views/discover/landing.tsx
+++ b/static/app/views/discover/landing.tsx
@@ -187,12 +187,10 @@ function DiscoverLanding() {
{
expect(measurementsMetaMock).toHaveBeenCalled();
});
- expect(screen.getByText('Discover')).toHaveAttribute(
+ expect(screen.getByRole('link', {name: 'Discover'})).toHaveAttribute(
'href',
expect.stringMatching(
new RegExp('^/organizations/org-slug/explore/discover/homepage/')
diff --git a/static/app/views/preprod/buildComparison/header/buildCompareHeaderContent.tsx b/static/app/views/preprod/buildComparison/header/buildCompareHeaderContent.tsx
index 92813e99c71226..21bea258819e36 100644
--- a/static/app/views/preprod/buildComparison/header/buildCompareHeaderContent.tsx
+++ b/static/app/views/preprod/buildComparison/header/buildCompareHeaderContent.tsx
@@ -37,14 +37,14 @@ export function BuildCompareHeaderContent(props: BuildCompareHeaderContentProps)
const breadcrumbs: Crumb[] = [
{
to: '#',
- label: 'Releases',
+ label: t('Releases'),
},
{
to: `/organizations/${organization.slug}/preprod/${projectId}/${buildDetails.id}/`,
- label: buildDetails.app_info.version,
+ label: buildDetails.app_info.version ?? t('Build Version'),
},
{
- label: 'Compare',
+ label: t('Compare'),
},
];
diff --git a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx
index 626e803804b67f..242e1eec305f24 100644
--- a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx
+++ b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx
@@ -69,7 +69,9 @@ export default function ReplayDetailsPageBreadcrumbs({readerResult}: Props) {
},
label: project ? (
- ) : null,
+ ) : (
+ t('Project')
+ ),
};
const replayCrumb = {
diff --git a/static/app/views/settings/components/settingsBreadcrumb/index.tsx b/static/app/views/settings/components/settingsBreadcrumb/index.tsx
index 47a39973f26cc5..e7a01d373aea67 100644
--- a/static/app/views/settings/components/settingsBreadcrumb/index.tsx
+++ b/static/app/views/settings/components/settingsBreadcrumb/index.tsx
@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
+import {trackAnalytics} from 'sentry/utils/analytics';
import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
import recreateRoute from 'sentry/utils/recreateRoute';
@@ -31,6 +32,10 @@ function SettingsBreadcrumb({className, routes, params}: Props) {
const lastRouteIndex = routes.map(r => !!r.name).lastIndexOf(true);
+ function onSettingsBreadcrumbLinkClick() {
+ trackAnalytics('breadcrumbs.link.clicked', {organization: null});
+ }
+
return (
{routes.map((route, i) => {
@@ -54,7 +59,10 @@ function SettingsBreadcrumb({className, routes, params}: Props) {
}
return (
-
+
{pathTitle || route.name}
{isLast ? null : }
diff --git a/static/app/views/settings/components/settingsBreadcrumb/organizationCrumb.tsx b/static/app/views/settings/components/settingsBreadcrumb/organizationCrumb.tsx
index 2eb46fefef64dc..e96c20cb3e6ecd 100644
--- a/static/app/views/settings/components/settingsBreadcrumb/organizationCrumb.tsx
+++ b/static/app/views/settings/components/settingsBreadcrumb/organizationCrumb.tsx
@@ -6,6 +6,7 @@ import IdBadge from 'sentry/components/idBadge';
import {t} from 'sentry/locale';
import OrganizationsStore from 'sentry/stores/organizationsStore';
import {useLegacyStore} from 'sentry/stores/useLegacyStore';
+import {trackAnalytics} from 'sentry/utils/analytics';
import recreateRoute from 'sentry/utils/recreateRoute';
import {resolveRoute} from 'sentry/utils/resolveRoute';
import {testableWindowLocation} from 'sentry/utils/testableWindowLocation';
@@ -76,6 +77,11 @@ function OrganizationCrumb({routes, route, ...props}: SettingsBreadcrumbProps) {
}
onCrumbSelect={handleSelect}
+ onOpenChange={open => {
+ if (open) {
+ trackAnalytics('breadcrumbs.menu.opened', {organization: null});
+ }
+ }}
hasMenu={hasMenu}
route={route}
value={organization.slug}
diff --git a/static/app/views/settings/components/settingsBreadcrumb/projectCrumb.tsx b/static/app/views/settings/components/settingsBreadcrumb/projectCrumb.tsx
index 0f7689eb1ebe55..6f10e1638e6ab6 100644
--- a/static/app/views/settings/components/settingsBreadcrumb/projectCrumb.tsx
+++ b/static/app/views/settings/components/settingsBreadcrumb/projectCrumb.tsx
@@ -4,6 +4,7 @@ import {ProjectAvatar} from 'sentry/components/core/avatar/projectAvatar';
import IdBadge from 'sentry/components/idBadge';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {space} from 'sentry/styles/space';
+import {trackAnalytics} from 'sentry/utils/analytics';
import recreateRoute from 'sentry/utils/recreateRoute';
import replaceRouterParams from 'sentry/utils/replaceRouterParams';
import {useNavigate} from 'sentry/utils/useNavigate';
@@ -65,6 +66,11 @@ function ProjectCrumb({routes, route, ...props}: SettingsBreadcrumbProps) {
}
value={activeProject?.slug ?? ''}
onCrumbSelect={handleSelect}
+ onOpenChange={open => {
+ if (open) {
+ trackAnalytics('breadcrumbs.menu.opened', {organization: null});
+ }
+ }}
onSearch={onSearch}
options={projects.map(project => ({
value: project.slug,
diff --git a/static/app/views/settings/components/settingsBreadcrumb/teamCrumb.tsx b/static/app/views/settings/components/settingsBreadcrumb/teamCrumb.tsx
index 6c210a254a90eb..81b6f011443b55 100644
--- a/static/app/views/settings/components/settingsBreadcrumb/teamCrumb.tsx
+++ b/static/app/views/settings/components/settingsBreadcrumb/teamCrumb.tsx
@@ -1,6 +1,7 @@
import {TeamAvatar} from 'sentry/components/core/avatar/teamAvatar';
import IdBadge from 'sentry/components/idBadge';
import {t} from 'sentry/locale';
+import {trackAnalytics} from 'sentry/utils/analytics';
import recreateRoute from 'sentry/utils/recreateRoute';
import {useNavigate} from 'sentry/utils/useNavigate';
import {useParams} from 'sentry/utils/useParams';
@@ -38,6 +39,11 @@ function TeamCrumb({routes, route, ...props}: SettingsBreadcrumbProps) {
})
);
}}
+ onOpenChange={open => {
+ if (open) {
+ trackAnalytics('breadcrumbs.menu.opened', {organization: null});
+ }
+ }}
hasMenu={hasMenu}
route={route}
value={team.slug}