Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion static/app/views/navigation/index.desktop.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,6 @@ describe('desktop navigation', () => {
[`${ORG}/insights/ai-agents/`, 'Insights', 'Agents'],
[`${ORG}/insights/mcp/`, 'Insights', 'MCP'],
[`${ORG}/monitors/crons/?insightsRedirect=true`, 'Monitors', 'Crons'],
[`${ORG}/insights/projects/`, 'Insights', 'All Projects'],
// Monitors
[`${ORG}/monitors/`, 'Monitors', 'All Monitors'],
[`${ORG}/monitors/my-monitors/`, 'Monitors', 'My Monitors'],
Expand Down
1 change: 1 addition & 0 deletions static/app/views/navigation/primaryNavigationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const PRIMARY_NAVIGATION_GROUP_CONFIG = {
explore: ['explore'],
dashboards: ['dashboards', 'dashboard'],
insights: ['insights'],
projects: ['projects'], // No primary nav button — accessed via logo nav. Needed for secondary nav routing.
monitors: ['monitors'],
settings: ['settings'],
prevent: ['prevent'],
Expand Down
3 changes: 3 additions & 0 deletions static/app/views/navigation/secondary/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {ExploreSecondaryNavigation} from 'sentry/views/navigation/secondary/sect
import {InsightsSecondaryNavigation} from 'sentry/views/navigation/secondary/sections/insights/insightsSecondaryNavigation';
import {IssuesSecondaryNavigation} from 'sentry/views/navigation/secondary/sections/issues/issuesSecondaryNavigation';
import {MonitorsSecondaryNavigation} from 'sentry/views/navigation/secondary/sections/monitors/monitorsSecondaryNavigation';
import {ProjectsSecondaryNavigation} from 'sentry/views/navigation/secondary/sections/projects/projectsSecondaryNavigation';
import {SettingsSecondaryNavigation} from 'sentry/views/navigation/secondary/sections/settings/settingsSecondaryNavigation';

export function SecondaryNavigationContent(): ReactNode {
Expand All @@ -21,6 +22,8 @@ export function SecondaryNavigationContent(): ReactNode {
return <DashboardsSecondaryNavigation />;
case 'explore':
return <ExploreSecondaryNavigation />;
case 'projects':
return <ProjectsSecondaryNavigation />;
case 'monitors':
return <MonitorsSecondaryNavigation />;
case 'prevent':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {Fragment, useMemo} from 'react';
import partition from 'lodash/partition';
import {Fragment} from 'react';

import Feature from 'sentry/components/acl/feature';
import {t} from 'sentry/locale';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useProjects} from 'sentry/utils/useProjects';
import {useUser} from 'sentry/utils/useUser';
import {makeMonitorBasePathname} from 'sentry/views/detectors/pathnames';
import {
Expand All @@ -29,23 +27,13 @@ import {
} from 'sentry/views/insights/pages/mobile/settings';
import {DOMAIN_VIEW_BASE_URL} from 'sentry/views/insights/pages/settings';
import {SecondaryNavigation} from 'sentry/views/navigation/secondary/components';
import {ProjectsNavigationItems} from 'sentry/views/navigation/secondary/sections/projects/starredProjectsList';

export function InsightsSecondaryNavigation() {
const user = useUser();
const organization = useOrganization();
const baseUrl = `/organizations/${organization.slug}/${DOMAIN_VIEW_BASE_URL}`;

const {projects} = useProjects();

const [starredProjects, nonStarredProjects] = useMemo(() => {
return partition(projects, project => project.isBookmarked);
}, [projects]);

const displayStarredProjects = starredProjects.length > 0;
const projectsToDisplay = displayStarredProjects
? starredProjects.slice(0, 8)
: nonStarredProjects.filter(project => project.isMember).slice(0, 8);

const shouldRedirectToMonitors =
organization.features.includes('workflow-engine-ui') && !user?.isStaff;

Expand Down Expand Up @@ -133,49 +121,15 @@ export function InsightsSecondaryNavigation() {
</Feature>
</SecondaryNavigation.List>
</SecondaryNavigation.Section>
<SecondaryNavigation.Separator />
<SecondaryNavigation.Section id="insights-projects-all">
<SecondaryNavigation.List>
<SecondaryNavigation.ListItem>
<SecondaryNavigation.Link
to={`${baseUrl}/projects/`}
end
analyticsItemName="insights_projects_all"
>
{t('All Projects')}
</SecondaryNavigation.Link>
</SecondaryNavigation.ListItem>
</SecondaryNavigation.List>
</SecondaryNavigation.Section>
{projectsToDisplay.length > 0 ? (
{!organization.features.includes('workflow-engine-ui') && (
<Fragment>
<SecondaryNavigation.Separator />
<SecondaryNavigation.Section
id="insights-starred-projects"
title={displayStarredProjects ? t('Starred Projects') : t('Projects')}
>
<SecondaryNavigation.List>
{projectsToDisplay.map(project => (
<SecondaryNavigation.ListItem key={project.id}>
<SecondaryNavigation.Link
to={`${baseUrl}/projects/${project.slug}/`}
leadingItems={
<SecondaryNavigation.ProjectIcon
projectPlatforms={
project.platform ? [project.platform] : ['default']
}
/>
}
analyticsItemName="insights_project_starred"
>
{project.slug}
</SecondaryNavigation.Link>
</SecondaryNavigation.ListItem>
))}
</SecondaryNavigation.List>
</SecondaryNavigation.Section>
<ProjectsNavigationItems
allProjectsAnalyticsItemName="insights_projects_all"
starredAnalyticsItemName="insights_project_starred"
/>
</Fragment>
) : null}
)}
</SecondaryNavigation.Body>
</Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Fragment} from 'react';

import {t} from 'sentry/locale';
import {SecondaryNavigation} from 'sentry/views/navigation/secondary/components';
import {ProjectsNavigationItems} from 'sentry/views/navigation/secondary/sections/projects/starredProjectsList';

export function ProjectsSecondaryNavigation() {
return (
<Fragment>
<SecondaryNavigation.Header>{t('Projects')}</SecondaryNavigation.Header>
<SecondaryNavigation.Body>
<ProjectsNavigationItems />
</SecondaryNavigation.Body>
</Fragment>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {Fragment, useMemo} from 'react';
import partition from 'lodash/partition';

import {t} from 'sentry/locale';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useProjects} from 'sentry/utils/useProjects';
import {SecondaryNavigation} from 'sentry/views/navigation/secondary/components';
import {makeProjectsPathname} from 'sentry/views/projects/pathname';

interface ProjectsNavigationItemsProps {
allProjectsAnalyticsItemName?: string;
starredAnalyticsItemName?: string;
}

export function ProjectsNavigationItems({
allProjectsAnalyticsItemName = 'projects_all',
starredAnalyticsItemName = 'project_starred',
}: ProjectsNavigationItemsProps) {
const organization = useOrganization();
const {projects} = useProjects();

const [starredProjects, nonStarredProjects] = useMemo(() => {
return partition(projects, project => project.isBookmarked);
}, [projects]);

const displayStarredProjects = starredProjects.length > 0;
const projectsToDisplay = displayStarredProjects
? starredProjects.slice(0, 8)
: nonStarredProjects.filter(project => project.isMember).slice(0, 8);

return (
<Fragment>
<SecondaryNavigation.Section id="projects-all">
<SecondaryNavigation.List>
<SecondaryNavigation.ListItem>
<SecondaryNavigation.Link
to={makeProjectsPathname({path: '/', organization})}
end
analyticsItemName={allProjectsAnalyticsItemName}
>
{t('All Projects')}
</SecondaryNavigation.Link>
</SecondaryNavigation.ListItem>
</SecondaryNavigation.List>
</SecondaryNavigation.Section>
{projectsToDisplay.length > 0 ? (
<Fragment>
<SecondaryNavigation.Separator />
<SecondaryNavigation.Section
id="starred-projects"
title={displayStarredProjects ? t('Starred Projects') : t('Projects')}
>
<SecondaryNavigation.List>
{projectsToDisplay.map(project => (
<SecondaryNavigation.ListItem key={project.id}>
<SecondaryNavigation.Link
to={makeProjectsPathname({
path: `/${project.slug}/`,
organization,
})}
leadingItems={
<SecondaryNavigation.ProjectIcon
projectPlatforms={
project.platform ? [project.platform] : ['default']
}
/>
}
analyticsItemName={starredAnalyticsItemName}
>
{project.slug}
</SecondaryNavigation.Link>
</SecondaryNavigation.ListItem>
))}
</SecondaryNavigation.List>
</SecondaryNavigation.Section>
</Fragment>
) : null}
</Fragment>
);
}
15 changes: 14 additions & 1 deletion static/app/views/projects/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import {Outlet} from 'react-router-dom';

import {Redirect} from 'sentry/components/redirect';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useRedirectNavigationV2Routes} from 'sentry/views/navigation/useRedirectNavigationV2Routes';

export default function Projects() {
const redirectPath = useRedirectNavigationV2Routes({
const organization = useOrganization();
// Both hooks are called unconditionally (React rules of hooks), but only one
// can match at a time since the current URL can't start with both prefixes.
// We select which result to act on based on the feature flag.
const forwardRedirect = useRedirectNavigationV2Routes({
oldPathPrefix: '/projects/',
newPathPrefix: '/insights/projects/',
});
const reverseRedirect = useRedirectNavigationV2Routes({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JonasBa you were trying to remove this redirect hook - do you think we should use something else for this particular behavior?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's fine to use. I'd probably rather add a root level redirect as I think that is where we keep some other similar logic.

@gggritso the redirect hook sends a log line to sentry so that we can track if or what routes it fires for. I may message you in the future about removing this if we see that's ok :)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course! I'm hoping that this redirection logic only stays around for a week or two before we just rip this code right out because Insights (and Projects) ar going away in favour of dashboards 🤞🏻

oldPathPrefix: '/insights/projects/',
newPathPrefix: '/projects/',
});

const redirectPath = organization.features.includes('workflow-engine-ui')
? reverseRedirect
: forwardRedirect;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spurious redirect warning logged on canonical project URLs

Medium Severity

Unconditionally calling both useRedirectNavigationV2Routes hooks causes the internally-fired useLogUnexpectedNavigationRedirect to log false 'Unexpected navigation redirect' warnings to Sentry. This occurs when the discarded hook's oldPathPrefix matches the current canonical URL, generating monitoring noise that may obscure legitimate issues.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit baa4036. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somewhat of a bummer but doesn't feel that important? Not sure! It's a very rare situation that we have two of those redirect hook in the same place, due to this conditional redirect!


if (redirectPath) {
return <Redirect to={redirectPath} />;
Expand Down
9 changes: 4 additions & 5 deletions static/app/views/projects/pathname.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type {Organization} from 'sentry/types/organization';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';

const PROJECTS_BASE_PATHNAME = 'insights/projects';

export function makeProjectsPathname({
path,
organization,
}: {
organization: Organization;
path: '/' | `/${string}/`;
}) {
return normalizeUrl(
`/organizations/${organization.slug}/${PROJECTS_BASE_PATHNAME}${path}`
);
const base = organization.features.includes('workflow-engine-ui')
? 'projects'
: 'insights/projects';
return normalizeUrl(`/organizations/${organization.slug}/${base}${path}`);
}
Loading