mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
Deprecate old org and project dropdowns, LayoutHeader to use the new ones under AppLayout
This commit is contained in:
1 parent
40d41d7fd3
commit
03f35e7ddb
7 files changed
+37
-211
No files matched your search
@@ -24,7 +24,11 @@ import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
|
||||
const OrganizationDropdown = () => {
|
||||
interface OrganizationDropdownProps {
|
||||
alt?: boolean // To distinguish slight behaviour change between nav v1 and v2, true for former
|
||||
}
|
||||
|
||||
const OrganizationDropdown = ({ alt = false }: OrganizationDropdownProps) => {
|
||||
const router = useRouter()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery()
|
||||
@@ -56,7 +60,7 @@ const OrganizationDropdown = () => {
|
||||
}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm">{orgName}</p>
|
||||
<p className={alt ? 'text-xs' : 'text-sm'}>{orgName}</p>
|
||||
{isSuccess && <Badge color="scale">{subscription?.plan.name}</Badge>}
|
||||
</div>
|
||||
</Button>
|
||||
@@ -72,6 +76,8 @@ const OrganizationDropdown = () => {
|
||||
{organizations?.map((org) => {
|
||||
const href = router.pathname.includes('[slug]')
|
||||
? router.pathname.replace('[slug]', org.slug)
|
||||
: alt
|
||||
? `/org/${org.slug}/general`
|
||||
: `/org/${org.slug}`
|
||||
return (
|
||||
<Link passHref href={href} key={org.slug}>
|
||||
|
||||
@@ -99,7 +99,11 @@ const ProjectLink = ({
|
||||
)
|
||||
}
|
||||
|
||||
const ProjectDropdown = () => {
|
||||
interface ProjectDropdownProps {
|
||||
alt?: boolean // To distinguish slight behaviour change between nav v1 and v2, true for former
|
||||
}
|
||||
|
||||
const ProjectDropdown = ({ alt = false }: ProjectDropdownProps) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const projectDetails = useSelectedProject()
|
||||
@@ -116,9 +120,11 @@ const ProjectDropdown = () => {
|
||||
{ projectRef: ref },
|
||||
{ enabled: !isOrgBilling }
|
||||
)
|
||||
const projects = allProjects
|
||||
?.filter((x) => x.organization_id === selectedOrganization?.id)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const projects = alt
|
||||
? allProjects?.sort((a, b) => a.name.localeCompare(b.name))
|
||||
: allProjects
|
||||
?.filter((x) => x.organization_id === selectedOrganization?.id)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const selectedProject = isBranch
|
||||
? parentProject
|
||||
: projectDetails || projects?.find((project) => project.ref === ref)
|
||||
@@ -139,7 +145,7 @@ const ProjectDropdown = () => {
|
||||
iconRight={<IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} />}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm">{selectedProject?.name}</p>
|
||||
<p className={alt ? 'text-xs' : 'text-sm'}>{selectedProject?.name}</p>
|
||||
{isSuccess && !isOrgBilling && <Badge color="slate">{subscription?.plan.name}</Badge>}
|
||||
</div>
|
||||
</Button>
|
||||
@@ -192,7 +198,7 @@ const ProjectDropdown = () => {
|
||||
</div>
|
||||
) : (
|
||||
<Button type="text">
|
||||
<span className="text-sm">{selectedProject?.name}</span>
|
||||
<span className={alt ? 'text-xs' : 'text-sm'}>{selectedProject?.name}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import { Badge } from 'ui'
|
||||
|
||||
import BranchDropdown from 'components/layouts/AppLayout/BranchDropdown'
|
||||
import EnableBranchingButton from 'components/layouts/AppLayout/EnableBranchingButton/EnableBranchingButton'
|
||||
import { getResourcesExceededLimits } from 'components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import OrganizationDropdown from 'components/layouts/AppLayout/OrganizationDropdown'
|
||||
import ProjectDropdown from 'components/layouts/AppLayout/ProjectDropdown'
|
||||
import {
|
||||
getResourcesExceededLimits,
|
||||
getResourcesExceededLimitsOrg,
|
||||
} from 'components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
|
||||
import { getResourcesExceededLimitsOrg } from 'components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
|
||||
|
||||
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
|
||||
import { useFlag, useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
@@ -18,8 +20,6 @@ import BreadcrumbsView from './BreadcrumbsView'
|
||||
import FeedbackDropdown from './FeedbackDropdown'
|
||||
import HelpPopover from './HelpPopover'
|
||||
import NotificationsPopover from './NotificationsPopover'
|
||||
import OrgDropdown from './OrgDropdown'
|
||||
import ProjectDropdown from './ProjectDropdown'
|
||||
|
||||
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
@@ -85,7 +85,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
{/* Organization is selected */}
|
||||
{projectRef && selectedOrganization ? (
|
||||
<>
|
||||
<OrgDropdown />
|
||||
<OrganizationDropdown alt />
|
||||
|
||||
{projectRef && (
|
||||
<>
|
||||
@@ -104,7 +104,8 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
<path d="M16 3.549L7.12 20.600"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<ProjectDropdown />
|
||||
|
||||
<ProjectDropdown alt />
|
||||
|
||||
{showOverUsageBadge && (
|
||||
<div className="ml-2">
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { toJS } from 'mobx'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useSelectedOrganization, useStore } from 'hooks'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { Button, Dropdown, IconPlus } from 'ui'
|
||||
|
||||
const OrgDropdown = () => {
|
||||
const router = useRouter()
|
||||
const { ui } = useStore()
|
||||
|
||||
const { data: organizations } = useOrganizationsQuery()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
return IS_PLATFORM ? (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={
|
||||
<>
|
||||
{organizations
|
||||
?.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((x) => {
|
||||
const slug = toJS(x.slug)
|
||||
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key={slug}
|
||||
onClick={() => {
|
||||
if (!slug) {
|
||||
// The user should not see this error as the page should
|
||||
// be rerendered with the value of slug before they can click.
|
||||
// It is just here in case they are the flash.
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message:
|
||||
'Could not navigate to organization settings, please try again or contact support',
|
||||
})
|
||||
}
|
||||
|
||||
router.push({
|
||||
pathname: `/org/[slug]/general`,
|
||||
query: { slug },
|
||||
hash: router.asPath.split('#')[1]?.toLowerCase(),
|
||||
})
|
||||
}}
|
||||
>
|
||||
{x.name}
|
||||
</Dropdown.Item>
|
||||
)
|
||||
})}
|
||||
<Dropdown.Separator />
|
||||
|
||||
<Dropdown.Item icon={<IconPlus size="tiny" />} onClick={() => router.push(`/new`)}>
|
||||
New organization
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button asChild type="text" size="tiny">
|
||||
<span>{selectedOrganization?.name}</span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button asChild type="text" size="tiny">
|
||||
<span>{selectedOrganization?.name}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgDropdown
|
||||
@@ -1,122 +0,0 @@
|
||||
import { useParams } from 'common'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import { Button, Dropdown, IconPlus, Popover } from 'ui'
|
||||
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { Organization, Project } from 'types'
|
||||
|
||||
// [Fran] the idea is to let users change projects without losing the current page,
|
||||
// but at the same time we need to redirect correctly between urls that might be
|
||||
// unique to a project e.g. '/project/projectRef/editor/tableId'
|
||||
// Right now, I'm gonna assume that any router query after the projectId,
|
||||
// is a unique project id/marker so we'll redirect the user to the
|
||||
// highest common route with just projectRef in the router queries.
|
||||
|
||||
export const sanitizeRoute = (route: string, routerQueries: ParsedUrlQuery) => {
|
||||
const queryArray = Object.entries(routerQueries)
|
||||
|
||||
if (queryArray.length > 1) {
|
||||
// [Joshen] Ideally we shouldn't use hard coded numbers, but temp workaround
|
||||
// for storage bucket route since its longer
|
||||
const isStorageBucketRoute = 'bucketId' in routerQueries
|
||||
return route
|
||||
.split('/')
|
||||
.slice(0, isStorageBucketRoute ? 5 : 4)
|
||||
.join('/')
|
||||
} else {
|
||||
return route
|
||||
}
|
||||
}
|
||||
|
||||
const ProjectLink = ({
|
||||
project,
|
||||
organization,
|
||||
}: {
|
||||
project: Project
|
||||
organization?: Organization
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const selectedProject = useSelectedProject()
|
||||
const sanitizedRoute = sanitizeRoute(router.route, router.query)
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
|
||||
// [Joshen] Temp while we're interim between v1 and v2 billing
|
||||
let href = sanitizedRoute?.replace('[ref]', project.ref) ?? `/project/${project.ref}`
|
||||
if (href.endsWith('settings/addons') && !isOrgBilling) {
|
||||
href = href.replace('settings/addons', 'settings/billing/subscription')
|
||||
} else if (href.endsWith('settings/billing/subscription') && isOrgBilling) {
|
||||
href = href.replace('settings/billing/subscription', 'settings/addons')
|
||||
} else if (href.endsWith('settings/infrastructure') && !isOrgBilling) {
|
||||
href = href.replace('settings/infrastructure', 'settings/billing/usage')
|
||||
} else if (href.endsWith('settings/billing/usage') && !isOrgBilling) {
|
||||
href = href.replace('settings/billing/usage', 'settings/infrastructure')
|
||||
}
|
||||
|
||||
return (
|
||||
<Link passHref href={href}>
|
||||
<a className="block">
|
||||
<Dropdown.Item
|
||||
className={
|
||||
selectedProject?.name === project.name ? 'font-bold bg-slate-400 dark:bg-slate-500' : ''
|
||||
}
|
||||
>
|
||||
{project.name}
|
||||
</Dropdown.Item>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const ProjectDropdown = () => {
|
||||
const { ref } = useParams()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const { data: allOrganizations } = useOrganizationsQuery()
|
||||
const { data: allProjects, isLoading: isLoadingProjects } = useProjectsQuery()
|
||||
|
||||
const selectedProjectName = allProjects?.find((project) => project.ref === ref)?.name
|
||||
const selectedOrganizationSlug = selectedOrganization?.slug
|
||||
|
||||
if (isLoadingProjects) {
|
||||
return <ShimmeringLoader className="w-[90px]" />
|
||||
}
|
||||
|
||||
return IS_PLATFORM ? (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
align="start"
|
||||
overlay={
|
||||
<>
|
||||
{allProjects
|
||||
?.filter((x) => x.status !== PROJECT_STATUS.INACTIVE)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((x) => {
|
||||
const org = allOrganizations?.find((org) => org.id === x.organization_id)
|
||||
return <ProjectLink key={x.ref} project={x} organization={org} />
|
||||
})}
|
||||
<Popover.Separator />
|
||||
<Link href={`/new/${selectedOrganizationSlug}`}>
|
||||
<a className="block">
|
||||
<Dropdown.Item icon={<IconPlus size="tiny" />}>New project</Dropdown.Item>
|
||||
</a>
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button type="text">
|
||||
<span className="text-xs">{selectedProjectName}</span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button type="text">
|
||||
<span className="text-xs">{selectedProjectName}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectDropdown
|
||||
@@ -6,7 +6,9 @@ import { useMemo } from 'react'
|
||||
export function useSelectedProject() {
|
||||
const { ref } = useParams()
|
||||
const { data } = useProjectDetailQuery({ ref })
|
||||
return data
|
||||
|
||||
const updatedData = { ...data, parentRef: data?.parent_project_ref ?? data?.ref ?? undefined }
|
||||
return updatedData
|
||||
}
|
||||
|
||||
export function useProjectByRef(ref?: string) {
|
||||
|
||||
@@ -42,6 +42,11 @@ export interface Project extends ProjectBase {
|
||||
* If not we will show ConnectingState and run a polling until it's back online
|
||||
*/
|
||||
postgrestStatus?: 'ONLINE' | 'OFFLINE'
|
||||
/**
|
||||
* Only available on client side only, for components that require the parentRef
|
||||
* irregardless of being on any branch, such as ProjectDropdown and Vercel integration
|
||||
* */
|
||||
parentRef?: string
|
||||
}
|
||||
|
||||
export interface User {
|
||||
|
||||
Reference in new issue
Block a user