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
2 changes: 1 addition & 1 deletion frontend/app/[team]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function RootLayout({
)}
>
{activeOrganisation && <UnlockKeyringDialog organisation={activeOrganisation} />}
{showNav && <NavBar team={params.team} />}
{showNav && <NavBar />}
{showNav && <Sidebar />}
<div className="grid h-screen">
<div></div>
Expand Down
49 changes: 33 additions & 16 deletions frontend/app/webauth/[requestCode]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

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'
Expand Down Expand Up @@ -31,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'

Expand Down Expand Up @@ -325,7 +323,8 @@ export default function WebAuth({ params }: { params: { requestCode: string } })
CLI Authentication failed
</h1>
<p className="text-neutral-500 text-base">
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:
</p>
</div>

Expand All @@ -336,7 +335,8 @@ export default function WebAuth({ params }: { params: { requestCode: string } })
1
</span>
<p className="text-black dark:text-white">
Exit out of the CLI by pressing <code className="font-mono font-bold">Ctrl+C</code>
Exit out of the CLI by pressing{' '}
<code className="font-mono font-bold">Ctrl+C</code>
</p>
</div>
</div>
Expand All @@ -346,7 +346,10 @@ export default function WebAuth({ params }: { params: { requestCode: string } })
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-neutral-500/20 text-sm font-medium">
2
</span>
<p className="text-black dark:text-white">Retry authentication manually via the <code className="font-mono font-bold">token</code> mode:</p>
<p className="text-black dark:text-white">
Retry authentication manually via the{' '}
<code className="font-mono font-bold">token</code> mode:
</p>
</div>
<CliCommand command="auth --mode token" />
<div className="pl-8 text-neutral-500 text-sm space-y-2">
Expand All @@ -356,7 +359,13 @@ export default function WebAuth({ params }: { params: { requestCode: string } })
Choose your Phase instance type as: <b>☁️ Phase Cloud</b>
</li>
<li>
Enter your email address: <code className="text-emerald-500 cursor-pointer font-mono" onClick={() => handleCopy(session?.user?.email || '')}>{session?.user?.email}</code>
Enter your email address:{' '}
<code
className="text-emerald-500 cursor-pointer font-mono"
onClick={() => handleCopy(session?.user?.email || '')}
>
{session?.user?.email}
</code>
</li>
</ul>
) : (
Expand All @@ -365,10 +374,22 @@ export default function WebAuth({ params }: { params: { requestCode: string } })
Choose your Phase instance type as: <b>🛠️ Self Hosted</b>
</li>
<li>
Enter the host: <code className="text-emerald-500 cursor-pointer font-mono" onClick={() => handleCopy(getHostname() || '')}>{getHostname()}</code>
Enter the host:{' '}
<code
className="text-emerald-500 cursor-pointer font-mono"
onClick={() => handleCopy(getHostname() || '')}
>
{getHostname()}
</code>
</li>
<li>
Enter your email address: <code className="text-emerald-500 cursor-pointer font-mono" onClick={() => handleCopy(session?.user?.email || '')}>{session?.user?.email}</code>
Enter your email address:{' '}
<code
className="text-emerald-500 cursor-pointer font-mono"
onClick={() => handleCopy(session?.user?.email || '')}
>
{session?.user?.email}
</code>
</li>
</ul>
)}
Expand All @@ -384,20 +405,16 @@ export default function WebAuth({ params }: { params: { requestCode: string } })
</p>
</div>
<div className="ph-no-capture">
<CliCommand
command={userToken}
prefix=""
wrap={true}
/>
<CliCommand command={userToken} prefix="" wrap={true} />
</div>
</div>
</div>

<div className="space-y-4 pt-16">
<div className="text-center">
<a
href="https://docs.phase.dev/cli/commands#auth"
target="_blank"
<a
href="https://docs.phase.dev/cli/commands#auth"
target="_blank"
rel="noreferrer"
className="text-sm text-emerald-500 hover:text-emerald-400 transition ease"
>
Expand Down
144 changes: 70 additions & 74 deletions frontend/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import UserMenu from '../UserMenu'
'use client'

import { useLazyQuery, useQuery } from '@apollo/client'
import { useContext, useEffect, useMemo } from 'react'
import Link from 'next/link'
import { startCase } from 'lodash'
import { useSearchParams } from 'next/navigation'

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 { generateBreadcrumbs, generatePageTitle, NavigationContext } from '@/utils/navigation'

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 UserMenu from '../UserMenu'

import { useParsedRoute } from '@/utils/route'

export const NavBar = (props: { team: string }) => {
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')

Expand All @@ -25,95 +35,81 @@ 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 envId = usePathname()?.split('/')[5]

const appPage = usePathname()?.split('/')[4]

const activeApp = orgContext === 'apps' ? apps?.find((app) => app.id === appId) : undefined
const activeApp = context === '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])
// Create navigation context
const navigationContext: NavigationContext = useMemo(
() => ({
team,
context,
appId,
envId,
page,
subPage,
activeApp,
activeEnv,
}),
[team, context, appId, envId, page, subPage, activeApp, activeEnv]
)

const activeEnv = activeApp ? envs.find((env) => env.id === envId) : undefined
const breadcrumbs = generateBreadcrumbs(navigationContext)

return (
<header className="pr-8 pl-4 w-full h-16 border-b border-neutral-500/20 fixed top-0 z-10 grid grid-cols-3 gap-4 items-center justify-between text-neutral-500 font-medium text-sm bg-neutral-100/70 dark:bg-neutral-800/20 backdrop-blur-md">
const BreadCrumbs = () => {
return (
<div className="flex items-center gap-2 min-w-0 overflow-hidden">
<Link href="/" className="shrink-0">
<LogoMark className="size-8 fill-black dark:fill-white" />
</Link>
<span className="shrink-0">/</span>

<Link
href={`/${props.team}`}
className={clsx(
'overflow-hidden text-ellipsis whitespace-nowrap',
orgContext ? 'text-neutral-500' : 'text-black dark:text-white'
)}
>
{props.team}
</Link>

{activeApp && <span className="shrink-0">/</span>}

{activeApp &&
(appPage ? (
<Link
href={`/${props.team}/apps/${activeApp.id}`}
className="overflow-hidden text-ellipsis whitespace-nowrap"
>
{activeApp.name}
</Link>
) : (
<span className="text-black dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
{activeApp.name}
</span>
))}

{activeApp && appPage && <span className="shrink-0">/</span>}

{activeApp && appPage && (
<span
className={clsx(
'capitalize overflow-hidden text-ellipsis whitespace-nowrap',
activeEnv ? 'text-neutral-500' : 'text-black dark:text-white'
{breadcrumbs.map((crumb, index) => (
<div key={index} className="flex items-center gap-2 text-xs">
<span className="shrink-0">/</span>
{crumb.isLink && crumb.href ? (
<Link
href={crumb.href}
className="capitalize overflow-hidden text-ellipsis whitespace-nowrap text-zinc-500"
>
{startCase(crumb.label)}
</Link>
) : (
<span className="capitalize text-zinc-900 dark:text-zinc-100 overflow-hidden text-ellipsis whitespace-nowrap">
{startCase(crumb.label)}
</span>
)}
>
{appPage}
</span>
)}
</div>
))}
</div>
)
}

{activeEnv && <span className="shrink-0">/</span>}
useEffect(() => {
if (activeApp) {
getAppEnvs({ variables: { appId: activeApp.id } })
}
}, [activeApp, getAppEnvs])

{activeEnv && (
<span className="text-black dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
{activeEnv.name}
</span>
)}
// Update page title using the utility
useEffect(() => {
document.title = generatePageTitle(navigationContext)
}, [navigationContext, tab])

{!activeApp && orgContext && <span className="shrink-0">/</span>}
{!activeApp && <span className="capitalize text-black dark:text-white">{orgContext}</span>}
</div>
return (
<header className="pr-8 pl-4 w-full h-16 border-b border-neutral-500/20 fixed top-0 z-10 grid grid-cols-3 gap-4 items-center justify-between text-neutral-500 font-medium text-sm bg-neutral-100/70 dark:bg-neutral-800/20 backdrop-blur-md">
<BreadCrumbs />

<div className="flex justify-center w-full">
<CommandPalette />
</div>

<div className="flex gap-4 items-center justify-end">
<StatusIndicator />

<Link href="https://docs.phase.dev" target="_blank" className="hidden lg:block">
<Button variant="secondary">Docs</Button>
</Link>
Expand Down
25 changes: 25 additions & 0 deletions frontend/components/layout/OnboardingNavbar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<header className="fixed z-20 w-full" data-testid="navbar">
<nav className="mx-auto flex w-full items-center justify-between p-4">
Expand Down
15 changes: 12 additions & 3 deletions frontend/utils/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,20 @@ export const stringContainsCharacters = (value: string) => {
return isValid
}

/**
* 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
Expand Down
Loading