diff --git a/static/app/views/navigation/index.desktop.spec.tsx b/static/app/views/navigation/index.desktop.spec.tsx index 414d53ea7b90de..612752eb989283 100644 --- a/static/app/views/navigation/index.desktop.spec.tsx +++ b/static/app/views/navigation/index.desktop.spec.tsx @@ -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'], diff --git a/static/app/views/navigation/primaryNavigationContext.tsx b/static/app/views/navigation/primaryNavigationContext.tsx index 378dcdf69aa0e5..5b5b2baf688f61 100644 --- a/static/app/views/navigation/primaryNavigationContext.tsx +++ b/static/app/views/navigation/primaryNavigationContext.tsx @@ -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'], diff --git a/static/app/views/navigation/secondary/content.tsx b/static/app/views/navigation/secondary/content.tsx index 16ddf8ae18cec1..30f4995fb3284d 100644 --- a/static/app/views/navigation/secondary/content.tsx +++ b/static/app/views/navigation/secondary/content.tsx @@ -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 { @@ -21,6 +22,8 @@ export function SecondaryNavigationContent(): ReactNode { return ; case 'explore': return ; + case 'projects': + return ; case 'monitors': return ; case 'prevent': diff --git a/static/app/views/navigation/secondary/sections/insights/insightsSecondaryNavigation.tsx b/static/app/views/navigation/secondary/sections/insights/insightsSecondaryNavigation.tsx index fa8bd2d91cea01..077bf900a9cf03 100644 --- a/static/app/views/navigation/secondary/sections/insights/insightsSecondaryNavigation.tsx +++ b/static/app/views/navigation/secondary/sections/insights/insightsSecondaryNavigation.tsx @@ -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 { @@ -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; @@ -133,49 +121,15 @@ export function InsightsSecondaryNavigation() { - - - - - - {t('All Projects')} - - - - - {projectsToDisplay.length > 0 ? ( + {!organization.features.includes('workflow-engine-ui') && ( - - - {projectsToDisplay.map(project => ( - - - } - analyticsItemName="insights_project_starred" - > - {project.slug} - - - ))} - - + - ) : null} + )} ); diff --git a/static/app/views/navigation/secondary/sections/projects/projectsSecondaryNavigation.tsx b/static/app/views/navigation/secondary/sections/projects/projectsSecondaryNavigation.tsx new file mode 100644 index 00000000000000..f6be8ee179576d --- /dev/null +++ b/static/app/views/navigation/secondary/sections/projects/projectsSecondaryNavigation.tsx @@ -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 ( + + {t('Projects')} + + + + + ); +} diff --git a/static/app/views/navigation/secondary/sections/projects/starredProjectsList.tsx b/static/app/views/navigation/secondary/sections/projects/starredProjectsList.tsx new file mode 100644 index 00000000000000..5659b4f1527a28 --- /dev/null +++ b/static/app/views/navigation/secondary/sections/projects/starredProjectsList.tsx @@ -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 ( + + + + + + {t('All Projects')} + + + + + {projectsToDisplay.length > 0 ? ( + + + + + {projectsToDisplay.map(project => ( + + + } + analyticsItemName={starredAnalyticsItemName} + > + {project.slug} + + + ))} + + + + ) : null} + + ); +} diff --git a/static/app/views/projects/index.tsx b/static/app/views/projects/index.tsx index c6d637f1509828..0ff20630db1201 100644 --- a/static/app/views/projects/index.tsx +++ b/static/app/views/projects/index.tsx @@ -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({ + oldPathPrefix: '/insights/projects/', + newPathPrefix: '/projects/', + }); + + const redirectPath = organization.features.includes('workflow-engine-ui') + ? reverseRedirect + : forwardRedirect; if (redirectPath) { return ; diff --git a/static/app/views/projects/pathname.tsx b/static/app/views/projects/pathname.tsx index 6ed6d1bb685a20..fd7ef740b62ebd 100644 --- a/static/app/views/projects/pathname.tsx +++ b/static/app/views/projects/pathname.tsx @@ -1,8 +1,6 @@ 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, @@ -10,7 +8,8 @@ export function makeProjectsPathname({ 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}`); }