From 711847af1ef34863992360224c3598d595295b07 Mon Sep 17 00:00:00 2001 From: Rohan Date: Wed, 9 Jul 2025 15:56:06 +0530 Subject: [PATCH 1/8] feat: set contextual titles --- frontend/components/layout/Navbar.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/components/layout/Navbar.tsx b/frontend/components/layout/Navbar.tsx index 728b0efcf..e7e33825b 100644 --- a/frontend/components/layout/Navbar.tsx +++ b/frontend/components/layout/Navbar.tsx @@ -13,6 +13,7 @@ import clsx from 'clsx' import { LogoMark } from '../common/LogoMark' import CommandPalette from '../common/CommandPalette' import { userHasPermission } from '@/utils/access/permissions' +import { startCase } from 'lodash' export const NavBar = (props: { team: string }) => { const { activeOrganisation: organisation } = useContext(organisationContext) @@ -41,12 +42,26 @@ export const NavBar = (props: { team: string }) => { const activeApp = orgContext === 'apps' ? apps?.find((app) => app.id === appId) : undefined + const activeEnv = activeApp ? envs.find((env) => env.id === envId) : undefined + useEffect(() => { if (activeApp) getAppEnvs({ variables: { appId: activeApp.id } }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeApp]) - const activeEnv = activeApp ? envs.find((env) => env.id === envId) : undefined + useEffect(() => { + let title = 'Phase' + if (activeEnv && activeApp) { + title = `${startCase(activeEnv.name)} – ${startCase(activeApp.name)} – ${startCase(props.team)} | Phase` + } else if (activeApp) { + title = `${startCase(activeApp.name)} – ${startCase(props.team)} | Phase` + } else if (orgContext) { + title = `${startCase(orgContext)} – ${startCase(props.team)} | Phase` + } else if (props.team) { + title = `${startCase(props.team)} | Phase` + } + document.title = title + }, [activeEnv, activeApp, orgContext, props.team]) return (
From 4b576a68bf440a0a52a7d8d5f43809883148577d Mon Sep 17 00:00:00 2001 From: Rohan Date: Mon, 4 Aug 2025 18:32:45 +0530 Subject: [PATCH 2/8] refactor: breadcrumbs and page titles from route params --- frontend/app/[team]/layout.tsx | 2 +- frontend/components/layout/Navbar.tsx | 189 ++++++++++++++------------ frontend/utils/copy.ts | 66 ++++----- frontend/utils/route.ts | 42 ++++++ 4 files changed, 182 insertions(+), 117 deletions(-) create mode 100644 frontend/utils/route.ts diff --git a/frontend/app/[team]/layout.tsx b/frontend/app/[team]/layout.tsx index c0c4a0e63..7718eef7b 100644 --- a/frontend/app/[team]/layout.tsx +++ b/frontend/app/[team]/layout.tsx @@ -52,7 +52,7 @@ export default function RootLayout({ )} > {activeOrganisation && } - {showNav && } + {showNav && } {showNav && }
diff --git a/frontend/components/layout/Navbar.tsx b/frontend/components/layout/Navbar.tsx index e7e33825b..d533b47a5 100644 --- a/frontend/components/layout/Navbar.tsx +++ b/frontend/components/layout/Navbar.tsx @@ -1,22 +1,29 @@ -import UserMenu from '../UserMenu' +'use client' + import { useLazyQuery, useQuery } from '@apollo/client' +import { useContext, useEffect } from 'react' +import Link from 'next/link' +import clsx from 'clsx' +import { startCase } from 'lodash' + import { GetApps } from '@/graphql/queries/getApps.gql' import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' -import { usePathname } from 'next/navigation' -import { useContext, useEffect } from 'react' import { AppType, EnvironmentType } from '@/apollo/graphql' -import Link from 'next/link' +import { organisationContext } from '@/contexts/organisationContext' +import { userHasPermission } from '@/utils/access/permissions' + import { Button } from '../common/Button' import { StatusIndicator } from '../common/StatusIndicator' -import { organisationContext } from '@/contexts/organisationContext' -import clsx from 'clsx' import { LogoMark } from '../common/LogoMark' import CommandPalette from '../common/CommandPalette' -import { userHasPermission } from '@/utils/access/permissions' -import { startCase } from 'lodash' +import UserMenu from '../UserMenu' -export const NavBar = (props: { team: string }) => { +import { useParsedRoute } from '@/utils/route' +import { isUUID } from '@/utils/copy' + +export const NavBar = () => { const { activeOrganisation: organisation } = useContext(organisationContext) + const { team, context, appId, envId, page, subPage } = useParsedRoute() const userCanReadApps = userHasPermission(organisation?.role?.permissions, 'Apps', 'read') @@ -26,101 +33,116 @@ export const NavBar = (props: { team: string }) => { }, skip: !organisation || !userCanReadApps, }) - const [getAppEnvs, { data: appEnvsData }] = useLazyQuery(GetAppEnvironments) - - const orgContext = usePathname()?.split('/')[2] - const apps = appsData?.apps as AppType[] + const [getAppEnvs, { data: appEnvsData }] = useLazyQuery(GetAppEnvironments) + const apps = (appsData?.apps as AppType[]) || [] const envs: EnvironmentType[] = appEnvsData?.appEnvironments ?? [] - const appId = usePathname()?.split('/')[3] + const activeApp = context === 'apps' ? apps.find((app) => app.id === appId) : undefined + const activeEnv = activeApp ? envs.find((env) => env.id === envId) : undefined - const envId = usePathname()?.split('/')[5] + const BreadCrumbs = () => { + type Breadcrumb = { label: string; href?: string; isLink?: boolean } - const appPage = usePathname()?.split('/')[4] + const breadcrumbs: Breadcrumb[] = [ + { label: team ?? '', href: `/${team}`, isLink: Boolean(team) }, + ] - const activeApp = orgContext === 'apps' ? apps?.find((app) => app.id === appId) : undefined + if (activeApp) { + breadcrumbs.push({ + label: activeApp.name, + href: page ? `/${team}/apps/${activeApp.id}` : undefined, + isLink: Boolean(page), + }) + } - const activeEnv = activeApp ? envs.find((env) => env.id === envId) : undefined + if (!activeApp && context && page === undefined) { + breadcrumbs.push({ + label: context, + isLink: false, + }) + } - useEffect(() => { - if (activeApp) getAppEnvs({ variables: { appId: activeApp.id } }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeApp]) + if (page) { + breadcrumbs.push({ + label: page, + isLink: Boolean(activeApp), + }) + } - useEffect(() => { - let title = 'Phase' - if (activeEnv && activeApp) { - title = `${startCase(activeEnv.name)} – ${startCase(activeApp.name)} – ${startCase(props.team)} | Phase` - } else if (activeApp) { - title = `${startCase(activeApp.name)} – ${startCase(props.team)} | Phase` - } else if (orgContext) { - title = `${startCase(orgContext)} – ${startCase(props.team)} | Phase` - } else if (props.team) { - title = `${startCase(props.team)} | Phase` + if (subPage && !isUUID(subPage)) { + breadcrumbs.push({ + label: subPage, + isLink: false, + }) } - document.title = title - }, [activeEnv, activeApp, orgContext, props.team]) - return ( -
+ if (activeEnv) { + breadcrumbs.push({ + label: activeEnv.name, + isLink: false, + }) + } + + return (
- / - - - {props.team} - - {activeApp && /} - - {activeApp && - (appPage ? ( - - {activeApp.name} - - ) : ( - - {activeApp.name} - - ))} - - {activeApp && appPage && /} - - {activeApp && appPage && ( - ( +
+ / + {crumb.isLink && crumb.href ? ( + + {startCase(crumb.label)} + + ) : ( + + {startCase(crumb.label)} + )} - > - {appPage} - - )} +
+ ))} +
+ ) + } + + useEffect(() => { + if (activeApp) { + getAppEnvs({ variables: { appId: activeApp.id } }) + } + }, [activeApp, getAppEnvs]) - {activeEnv && /} + useEffect(() => { + let title = 'Phase' - {activeEnv && ( - - {activeEnv.name} - - )} + if (activeEnv && activeApp) { + title = `${startCase(activeEnv.name)} – ${startCase(activeApp.name)} – ${startCase(team)} | Phase` + } else if (activeApp && subPage) { + title = `${startCase(subPage)} – ${startCase(activeApp.name)} – ${startCase(team)} | Phase` + } else if (activeApp && page) { + title = `${startCase(page)} – ${startCase(activeApp.name)} – ${startCase(team)} | Phase` + } else if (activeApp) { + title = `${startCase(activeApp.name)} – ${startCase(team)} | Phase` + } else if (page && context) { + title = `${startCase(page)} – ${startCase(context)} – ${startCase(team)} | Phase` + } else if (context) { + title = `${startCase(context)} – ${startCase(team)} | Phase` + } else if (team) { + title = `${startCase(team)} | Phase` + } - {!activeApp && orgContext && /} - {!activeApp && {orgContext}} -
+ document.title = title + }, [activeApp, activeEnv, team, context, page, subPage]) + + return ( +
+
@@ -128,7 +150,6 @@ export const NavBar = (props: { team: string }) => {
- diff --git a/frontend/utils/copy.ts b/frontend/utils/copy.ts index 7cccfde30..5f2d04e2d 100644 --- a/frontend/utils/copy.ts +++ b/frontend/utils/copy.ts @@ -27,8 +27,8 @@ export const colors = [ * @returns {string} The space-separated string. */ export const camelCaseToSpaces = (str: string): string => { - return str.replace(/([a-z])([A-Z])/g, '$1 $2'); -}; + return str.replace(/([a-z])([A-Z])/g, '$1 $2') +} /** * Generates a hex color code from a given string. @@ -39,19 +39,19 @@ export const camelCaseToSpaces = (str: string): string => { */ export const stringToHexColor = (input: string): string => { // Simple hash function to generate a consistent hash from the input string - let hash = 0; + let hash = 0 for (let i = 0; i < input.length; i++) { - hash = input.charCodeAt(i) + ((hash << 5) - hash); + hash = input.charCodeAt(i) + ((hash << 5) - hash) } // Convert the hash to a hex color code - let color = '#'; + let color = '#' for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 0xFF; - color += ('00' + value.toString(16)).slice(-2); + const value = (hash >> (i * 8)) & 0xff + color += ('00' + value.toString(16)).slice(-2) } - return color; + return color } /** @@ -62,27 +62,27 @@ export const stringToHexColor = (input: string): string => { */ export const getContrastingTextColor = (hexColor: string): string => { // Convert hex to RGB - const r = parseInt(hexColor.slice(1, 3), 16); - const g = parseInt(hexColor.slice(3, 5), 16); - const b = parseInt(hexColor.slice(5, 7), 16); + const r = parseInt(hexColor.slice(1, 3), 16) + const g = parseInt(hexColor.slice(3, 5), 16) + const b = parseInt(hexColor.slice(5, 7), 16) // Calculate luminance - const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 // Return black or white depending on luminance - return luminance > 0.5 ? 'black' : 'white'; -}; + return luminance > 0.5 ? 'black' : 'white' +} /** -* Generates a random hexadecimal color string. -* -* @returns {string} A string representing a random hex color in the format "#RRGGBB". -*/ + * Generates a random hexadecimal color string. + * + * @returns {string} A string representing a random hex color in the format "#RRGGBB". + */ export const generateRandomHexColor = (): string => { - // Generate a random number between 0 and 0xFFFFFF, then convert to a hexadecimal string - const randomColor = Math.floor(Math.random() * 0xffffff).toString(16) - // Pad the string with leading zeros if necessary to ensure it has a length of 6 characters - return '#' + randomColor.padStart(6, '0') + // Generate a random number between 0 and 0xFFFFFF, then convert to a hexadecimal string + const randomColor = Math.floor(Math.random() * 0xffffff).toString(16) + // Pad the string with leading zeros if necessary to ensure it has a length of 6 characters + return '#' + randomColor.padStart(6, '0') } /** @@ -91,21 +91,23 @@ export const generateRandomHexColor = (): string => { * @returns {string} A random color in hex format. */ export const getRandomCuratedColor = (): string => { - const randomIndex = Math.floor(Math.random() * colors.length); - return colors[randomIndex]; -}; - - + const randomIndex = Math.floor(Math.random() * colors.length) + return colors[randomIndex] +} /** * Checks if a string contains at least one non-space character - * - * @param {string} value + * + * @param {string} value * @returns {boolean} */ export const stringContainsCharacters = (value: string) => { - const trimmedValue = value.trim(); // Trim leading and trailing spaces - const isValid = /^(?!\s*$).+/.test(trimmedValue); // Validate using the regex + const trimmedValue = value.trim() // Trim leading and trailing spaces + const isValid = /^(?!\s*$).+/.test(trimmedValue) // Validate using the regex return isValid -}; \ No newline at end of file +} + +export const isUUID = (value: string) => { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) +} diff --git a/frontend/utils/route.ts b/frontend/utils/route.ts new file mode 100644 index 000000000..d7d179e83 --- /dev/null +++ b/frontend/utils/route.ts @@ -0,0 +1,42 @@ +'use client' + +import { useParams, usePathname } from 'next/navigation' + +export function useParsedRoute() { + const params = useParams() + const pathname = usePathname() + const segments = pathname?.split('/').filter(Boolean) || [] + + const team = params?.team as string | undefined + + const context = segments[1] // apps, access, integrations, etc. + + const appId = context === 'apps' ? segments[2] : undefined + + // envId only appears for: /apps/[app]/environments/[envId]/... + const envId = context === 'apps' && segments[3] === 'environments' ? segments[4] : undefined + + const page = + context === 'apps' + ? segments[3] === 'environments' + ? segments[5] // in /apps/[app]/environments/[envId]/ + : segments[3] // in /apps/[app]/ + : segments[2] // in /access/, /integrations/ + + const subPage = + context === 'apps' + ? segments[3] === 'environments' + ? segments[6] // deeper nesting + : segments[4] + : segments[3] + + return { + pathname, + team, + context, // apps | access | integrations + appId, // uuid if apps + envId, // uuid if environments route + page, // syncing | logs | members | etc. + subPage, // tokens | additional nesting + } +} From a5aa70f4810f249647c16f2bc79a3f919629d12d Mon Sep 17 00:00:00 2001 From: Nimish Date: Sat, 30 Aug 2025 18:52:31 +0530 Subject: [PATCH 3/8] fix: imports and lint errors --- frontend/app/webauth/[requestCode]/page.tsx | 48 +++++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/frontend/app/webauth/[requestCode]/page.tsx b/frontend/app/webauth/[requestCode]/page.tsx index eb0cc1312..7032a1743 100644 --- a/frontend/app/webauth/[requestCode]/page.tsx +++ b/frontend/app/webauth/[requestCode]/page.tsx @@ -1,5 +1,6 @@ 'use client' +import React from 'react' import { OrganisationType } from '@/apollo/graphql' import { Button } from '@/components/common/Button' import { HeroPattern } from '@/components/common/HeroPattern' @@ -325,7 +326,8 @@ export default function WebAuth({ params }: { params: { requestCode: string } }) CLI Authentication failed

- CLI authentication could not be completed from this page. Please follow these steps to retry the authentication: + CLI authentication could not be completed from this page. Please follow these steps to + retry the authentication:

@@ -336,7 +338,8 @@ export default function WebAuth({ params }: { params: { requestCode: string } }) 1

- Exit out of the CLI by pressing Ctrl+C + Exit out of the CLI by pressing{' '} + Ctrl+C

@@ -346,7 +349,10 @@ export default function WebAuth({ params }: { params: { requestCode: string } }) 2 -

Retry authentication manually via the token mode:

+

+ Retry authentication manually via the{' '} + token mode: +

@@ -356,7 +362,13 @@ export default function WebAuth({ params }: { params: { requestCode: string } }) Choose your Phase instance type as: ☁️ Phase Cloud
  • - Enter your email address: handleCopy(session?.user?.email || '')}>{session?.user?.email} + Enter your email address:{' '} + handleCopy(session?.user?.email || '')} + > + {session?.user?.email} +
  • ) : ( @@ -365,10 +377,22 @@ export default function WebAuth({ params }: { params: { requestCode: string } }) Choose your Phase instance type as: 🛠️ Self Hosted
  • - Enter the host: handleCopy(getHostname() || '')}>{getHostname()} + Enter the host:{' '} + handleCopy(getHostname() || '')} + > + {getHostname()} +
  • - Enter your email address: handleCopy(session?.user?.email || '')}>{session?.user?.email} + Enter your email address:{' '} + handleCopy(session?.user?.email || '')} + > + {session?.user?.email} +
  • )} @@ -384,20 +408,16 @@ export default function WebAuth({ params }: { params: { requestCode: string } })

    - +
    - From 8272beeadbc6a729761f261b6b24d7f00248b033 Mon Sep 17 00:00:00 2001 From: Nimish Date: Sat, 30 Aug 2025 18:53:28 +0530 Subject: [PATCH 4/8] fix: organization settings page breadcrumb on tab switch --- frontend/components/layout/Navbar.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/components/layout/Navbar.tsx b/frontend/components/layout/Navbar.tsx index d533b47a5..723583328 100644 --- a/frontend/components/layout/Navbar.tsx +++ b/frontend/components/layout/Navbar.tsx @@ -3,8 +3,8 @@ import { useLazyQuery, useQuery } from '@apollo/client' import { useContext, useEffect } from 'react' import Link from 'next/link' -import clsx from 'clsx' import { startCase } from 'lodash' +import { useSearchParams } from 'next/navigation' import { GetApps } from '@/graphql/queries/getApps.gql' import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' @@ -24,6 +24,8 @@ import { isUUID } from '@/utils/copy' export const NavBar = () => { const { activeOrganisation: organisation } = useContext(organisationContext) const { team, context, appId, envId, page, subPage } = useParsedRoute() + const searchParams = useSearchParams() + const tab = searchParams?.get('tab') const userCanReadApps = userHasPermission(organisation?.role?.permissions, 'Apps', 'read') @@ -138,7 +140,7 @@ export const NavBar = () => { } document.title = title - }, [activeApp, activeEnv, team, context, page, subPage]) + }, [activeApp, activeEnv, team, context, page, subPage, tab]) return (
    From e2fc84333e7f54c52535d39bcf813780e2972d00 Mon Sep 17 00:00:00 2001 From: rohan Date: Mon, 6 Oct 2025 20:12:08 +0530 Subject: [PATCH 5/8] fix: syntax, jsdoc Signed-off-by: rohan --- frontend/utils/copy.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/utils/copy.ts b/frontend/utils/copy.ts index ad082cb28..0c0ee94eb 100644 --- a/frontend/utils/copy.ts +++ b/frontend/utils/copy.ts @@ -108,13 +108,20 @@ export const stringContainsCharacters = (value: string) => { return isValid } -export const isUUID = (value: string) => { - return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) +/** + * Regex to check if a string is a valid UUID + * + * @param {string} value + * @returns {boolean} + */ +export const isUUID = (value: string) => + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) + /** * Generates a random alphanumeric string of specified length. - * @param {string} min Minimum length of the string (default is 6) - * @param {string} max Maximum length of the string (default is 18) - * @returns A random string + * @param {number} min Minimum length of the string (default is 6) + * @param {number} max Maximum length of the string (default is 18) + * @returns {string} A random string */ export const randomString = (min = 6, max = 18) => { const length = Math.floor(Math.random() * (max - min + 1)) + min From ead3d2ba0e0cba6243492bafcdac3e43d38b23e1 Mon Sep 17 00:00:00 2001 From: rohan Date: Mon, 6 Oct 2025 20:52:03 +0530 Subject: [PATCH 6/8] refactor: add navigation utilities for breadcrumbs and page title generation Signed-off-by: rohan --- frontend/components/layout/Navbar.tsx | 86 ++++++---------------- frontend/utils/navigation.ts | 100 ++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 64 deletions(-) create mode 100644 frontend/utils/navigation.ts diff --git a/frontend/components/layout/Navbar.tsx b/frontend/components/layout/Navbar.tsx index 723583328..6350df6ef 100644 --- a/frontend/components/layout/Navbar.tsx +++ b/frontend/components/layout/Navbar.tsx @@ -1,7 +1,7 @@ 'use client' import { useLazyQuery, useQuery } from '@apollo/client' -import { useContext, useEffect } from 'react' +import { useContext, useEffect, useMemo } from 'react' import Link from 'next/link' import { startCase } from 'lodash' import { useSearchParams } from 'next/navigation' @@ -11,6 +11,7 @@ import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments import { AppType, EnvironmentType } from '@/apollo/graphql' import { organisationContext } from '@/contexts/organisationContext' import { userHasPermission } from '@/utils/access/permissions' +import { generateBreadcrumbs, generatePageTitle, NavigationContext } from '@/utils/navigation' import { Button } from '../common/Button' import { StatusIndicator } from '../common/StatusIndicator' @@ -19,7 +20,6 @@ import CommandPalette from '../common/CommandPalette' import UserMenu from '../UserMenu' import { useParsedRoute } from '@/utils/route' -import { isUUID } from '@/utils/copy' export const NavBar = () => { const { activeOrganisation: organisation } = useContext(organisationContext) @@ -44,49 +44,24 @@ export const NavBar = () => { const activeApp = context === 'apps' ? apps.find((app) => app.id === appId) : undefined const activeEnv = activeApp ? envs.find((env) => env.id === envId) : undefined - const BreadCrumbs = () => { - type Breadcrumb = { label: string; href?: string; isLink?: boolean } - - const breadcrumbs: Breadcrumb[] = [ - { label: team ?? '', href: `/${team}`, isLink: Boolean(team) }, - ] - - if (activeApp) { - breadcrumbs.push({ - label: activeApp.name, - href: page ? `/${team}/apps/${activeApp.id}` : undefined, - isLink: Boolean(page), - }) - } - - if (!activeApp && context && page === undefined) { - breadcrumbs.push({ - label: context, - isLink: false, - }) - } - - if (page) { - breadcrumbs.push({ - label: page, - isLink: Boolean(activeApp), - }) - } - - if (subPage && !isUUID(subPage)) { - breadcrumbs.push({ - label: subPage, - isLink: false, - }) - } + // Create navigation context + const navigationContext: NavigationContext = useMemo( + () => ({ + team, + context, + appId, + envId, + page, + subPage, + activeApp, + activeEnv, + }), + [team, context, appId, envId, page, subPage, activeApp, activeEnv] + ) - if (activeEnv) { - breadcrumbs.push({ - label: activeEnv.name, - isLink: false, - }) - } + const breadcrumbs = generateBreadcrumbs(navigationContext) + const BreadCrumbs = () => { return (
    @@ -94,7 +69,7 @@ export const NavBar = () => { {breadcrumbs.map((crumb, index) => ( -
    +
    / {crumb.isLink && crumb.href ? ( { } }, [activeApp, getAppEnvs]) + // Update page title using the utility useEffect(() => { - let title = 'Phase' - - if (activeEnv && activeApp) { - title = `${startCase(activeEnv.name)} – ${startCase(activeApp.name)} – ${startCase(team)} | Phase` - } else if (activeApp && subPage) { - title = `${startCase(subPage)} – ${startCase(activeApp.name)} – ${startCase(team)} | Phase` - } else if (activeApp && page) { - title = `${startCase(page)} – ${startCase(activeApp.name)} – ${startCase(team)} | Phase` - } else if (activeApp) { - title = `${startCase(activeApp.name)} – ${startCase(team)} | Phase` - } else if (page && context) { - title = `${startCase(page)} – ${startCase(context)} – ${startCase(team)} | Phase` - } else if (context) { - title = `${startCase(context)} – ${startCase(team)} | Phase` - } else if (team) { - title = `${startCase(team)} | Phase` - } - - document.title = title - }, [activeApp, activeEnv, team, context, page, subPage, tab]) + document.title = generatePageTitle(navigationContext) + }, [navigationContext, tab]) return (
    diff --git a/frontend/utils/navigation.ts b/frontend/utils/navigation.ts new file mode 100644 index 000000000..1cd5afb41 --- /dev/null +++ b/frontend/utils/navigation.ts @@ -0,0 +1,100 @@ +import { startCase } from 'lodash' +import { AppType, EnvironmentType } from '@/apollo/graphql' +import { isUUID } from '@/utils/copy' + +export type NavigationItem = { + label: string + href?: string + isLink?: boolean +} + +export type NavigationContext = { + team?: string | null + context?: string | null + appId?: string | null + envId?: string | null + page?: string | null + subPage?: string | null + activeApp?: AppType + activeEnv?: EnvironmentType +} + +export const generateBreadcrumbs = (ctx: NavigationContext): NavigationItem[] => { + const { team, context, page, subPage, activeApp, activeEnv } = ctx + + const breadcrumbs: NavigationItem[] = [ + { label: team ?? '', href: `/${team}`, isLink: Boolean(team) }, + ] + + if (activeApp) { + // App name should only be clickable if we're not at the app home + const isAtAppHome = !page && !activeEnv + breadcrumbs.push({ + label: activeApp.name, + href: isAtAppHome ? undefined : `/${team}/apps/${activeApp.id}`, + isLink: !isAtAppHome, // Only clickable when not at app home + }) + } + + if (!activeApp && context && page === undefined) { + breadcrumbs.push({ + label: context, + isLink: false, + }) + } + + // Handle different app sections + if (page) { + if (page === 'environments' && activeEnv) { + // For environment routes: /apps/[app]/environments/[environment]/[...path] + breadcrumbs.push({ + label: 'environments', + href: activeApp ? `/${team}/apps/${activeApp.id}` : undefined, + isLink: Boolean(activeApp), + }) + breadcrumbs.push({ + label: activeEnv.name, + isLink: false, + }) + // Add folder path if present + if (subPage && !isUUID(subPage)) { + breadcrumbs.push({ + label: subPage, + isLink: false, + }) + } + } else { + // For other app routes: /apps/[app]/[page] + breadcrumbs.push({ + label: page, + isLink: Boolean(activeApp), + }) + // Add subPage if present and not in environments + if (subPage && !isUUID(subPage)) { + breadcrumbs.push({ + label: subPage, + isLink: false, + }) + } + } + } else if (activeEnv && !page) { + breadcrumbs.push({ + label: activeEnv.name, + isLink: false, + }) + } + + return breadcrumbs +} + +export const generatePageTitle = (ctx: NavigationContext): string => { + const breadcrumbs = generateBreadcrumbs(ctx) + + // Filter out empty labels and reverse for title (most specific first) + const titleParts = breadcrumbs + .filter((crumb) => crumb.label && crumb.label.trim()) + .map((crumb) => startCase(crumb.label)) + .reverse() + + return `${titleParts.join(' · ')} | Phase Console` +} From 2ba348874f84fa5aee495fd4a0d735be5f3ef559 Mon Sep 17 00:00:00 2001 From: rohan Date: Mon, 6 Oct 2025 20:55:09 +0530 Subject: [PATCH 7/8] chore: cleanup unused imports Signed-off-by: rohan --- frontend/app/webauth/[requestCode]/page.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/app/webauth/[requestCode]/page.tsx b/frontend/app/webauth/[requestCode]/page.tsx index 7032a1743..9013555ed 100644 --- a/frontend/app/webauth/[requestCode]/page.tsx +++ b/frontend/app/webauth/[requestCode]/page.tsx @@ -1,9 +1,7 @@ 'use client' -import React from 'react' import { OrganisationType } from '@/apollo/graphql' import { Button } from '@/components/common/Button' -import { HeroPattern } from '@/components/common/HeroPattern' import { Input } from '@/components/common/Input' import Spinner from '@/components/common/Spinner' import OnboardingNavbar from '@/components/layout/OnboardingNavbar' @@ -32,7 +30,6 @@ import { useSession } from 'next-auth/react' import { useRouter } from 'next/navigation' import { useContext, useEffect, useState } from 'react' import { FaChevronRight, FaExclamationTriangle, FaCheckCircle, FaShieldAlt } from 'react-icons/fa' -import { MdContentCopy } from 'react-icons/md' import { SiGithub, SiGnometerminal, SiSlack } from 'react-icons/si' import { toast } from 'react-toastify' From 7a5274391ac2ddad80e52a10fdec621cc72b5cc0 Mon Sep 17 00:00:00 2001 From: rohan Date: Mon, 6 Oct 2025 21:19:52 +0530 Subject: [PATCH 8/8] feat: add titles for non org routes Signed-off-by: rohan --- .../components/layout/OnboardingNavbar.tsx | 25 ++++++++++++ frontend/utils/navigation.ts | 40 ++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/frontend/components/layout/OnboardingNavbar.tsx b/frontend/components/layout/OnboardingNavbar.tsx index 9393f59e7..e171cd348 100644 --- a/frontend/components/layout/OnboardingNavbar.tsx +++ b/frontend/components/layout/OnboardingNavbar.tsx @@ -1,8 +1,33 @@ +'use client' + import Link from 'next/link' +import { useEffect } from 'react' +import { usePathname } from 'next/navigation' import UserMenu from '../UserMenu' import { LogoWordMark } from '../common/LogoWordMark' +import { generatePageTitle } from '@/utils/navigation' const OnboardingNavbar = () => { + const pathname = usePathname() + + useEffect(() => { + // Parse the current route to determine page title + const pathSegments = (pathname ?? '').split('/').filter(Boolean) + let route = null + let routeParam = null + + if (pathSegments.length > 0) { + route = pathSegments[0] + if (pathSegments.length > 1) { + routeParam = pathSegments[1] + } + } + + // Set page title based on route + const title = generatePageTitle({ route, routeParam }) + document.title = title + }, [pathname]) + return (