mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Merge pull request #15884 from supabase/feat/nav-layout-change
feat/nav layout change
This commit is contained in:
50 files changed
+1555
-695
No files matched your search
Generated
+10
-222
@@ -18056,11 +18056,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase": {
|
||||
"version": "0.2.2",
|
||||
"license": "MIT",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cmdk-supabase/-/cmdk-supabase-1.0.7.tgz",
|
||||
"integrity": "sha512-5Ts9dZTLSqNwCEFzps/dtNK6Q1baje1JuA+5qjEvtfkk+SGZG0S5D1uqxbAk7YQqF/ctGZHxe8O+0+vNPYMk2A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-id": "^0.0.6",
|
||||
"@radix-ui/react-slot": "1.0.2",
|
||||
"command-score": "0.1.2",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
@@ -18069,112 +18071,10 @@
|
||||
"react-dom": "^17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.0.0",
|
||||
"@radix-ui/react-focus-guards": "1.0.0",
|
||||
"@radix-ui/react-focus-scope": "1.0.0",
|
||||
"@radix-ui/react-id": "1.0.0",
|
||||
"@radix-ui/react-portal": "1.0.0",
|
||||
"@radix-ui/react-presence": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.0",
|
||||
"@radix-ui/react-slot": "1.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.5.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0",
|
||||
"@radix-ui/react-use-escape-keydown": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-id": {
|
||||
"version": "0.0.6",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.0.6.tgz",
|
||||
"integrity": "sha512-PzmraF34fYggsYvTIZVJ5S68WMp3aKUN3HkSmGnz4zn9zpRjkAbbg7Xn3ueQI3FQsLWKgyUfnpsmWFDndpcqYg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
@@ -18182,119 +18082,6 @@
|
||||
"react": "^16.8 || ^17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk-supabase/node_modules/react-remove-scroll": {
|
||||
"version": "2.5.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.3",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"dev": true,
|
||||
@@ -18437,7 +18224,8 @@
|
||||
},
|
||||
"node_modules/command-score": {
|
||||
"version": "0.1.2",
|
||||
"license": "MIT"
|
||||
"resolved": "https://registry.npmjs.org/command-score/-/command-score-0.1.2.tgz",
|
||||
"integrity": "sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w=="
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "6.2.1",
|
||||
@@ -40809,7 +40597,7 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk-supabase": "^0.2.2",
|
||||
"cmdk-supabase": "^1.0.7",
|
||||
"color": "^4.2.3",
|
||||
"colorjs.io": "^0.4.3",
|
||||
"common-tags": "^1.8.2",
|
||||
|
||||
@@ -139,6 +139,13 @@ export {
|
||||
PopoverContent as PopoverContent_Shadcn_,
|
||||
} from './src/components/shadcn/ui/popover'
|
||||
|
||||
export {
|
||||
Accordion as Accordion_Shadcn_,
|
||||
AccordionItem as AccordionItem_Shadcn_,
|
||||
AccordionTrigger as AccordionTrigger_Shadcn_,
|
||||
AccordionContent as AccordionContent_Shadcn_,
|
||||
} from './src/components/shadcn/ui/accordion'
|
||||
|
||||
// links
|
||||
|
||||
export * from './src/components/TextLink'
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk-supabase": "^0.2.2",
|
||||
"cmdk-supabase": "^1.0.7",
|
||||
"color": "^4.2.3",
|
||||
"colorjs.io": "^0.4.3",
|
||||
"common-tags": "^1.8.2",
|
||||
|
||||
@@ -103,7 +103,7 @@ export function Item({ icon, className, ...props }: ItemProps) {
|
||||
{...props}
|
||||
>
|
||||
{icon && icon}
|
||||
<span>{props.children}</span>
|
||||
<span className="w-full">{props.children}</span>
|
||||
</RadixDropdown.Item>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ AccordionItem.displayName = 'AccordionItem'
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & { hideIcon?: boolean }
|
||||
>(({ className, children, hideIcon, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
@@ -28,7 +28,7 @@ const AccordionTrigger = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 transition-transform duration-200" />
|
||||
{!hideIcon && <ChevronDown className="h-4 w-4 transition-transform duration-200" />}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
|
||||
@@ -70,8 +70,12 @@ CommandList.displayName = CommandPrimitive.List.displayName
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className={cn('py-6 text-center text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
|
||||
@@ -153,7 +153,7 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -25,7 +25,7 @@ const AccessTokenList = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-7xl">
|
||||
<div>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="header-token">Token</Table.th>,
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import IncidentBanner from 'components/layouts/AppLayout/IncidentBanner'
|
||||
import { useFlag } from 'hooks'
|
||||
import { IconExternalLink } from 'ui'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
const AppBannerWrapper: FC = ({ children }) => {
|
||||
const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
|
||||
return (
|
||||
<div className="min-h-full flex flex-col">
|
||||
{ongoingIncident && (
|
||||
<Link href="https://status.supabase.com">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<div className="flex cursor-pointer items-center justify-center space-x-2 bg-green-900 py-3 text-scale-400 transition hover:bg-green-1000 dark:text-scale-1200">
|
||||
<p className="text-sm font-medium">
|
||||
We are currently investigating a technical issue, follow status.supabase.com for
|
||||
updates
|
||||
</p>
|
||||
<IconExternalLink size={16} strokeWidth={2} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{ongoingIncident && <IncidentBanner />}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -4,16 +4,17 @@ import { PropsWithChildren, useEffect } from 'react'
|
||||
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import useLatest from 'hooks/misc/useLatest'
|
||||
import { useParams } from 'common'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
|
||||
// Ideally these could all be within a _middleware when we use Next 12
|
||||
const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
const { ui } = useStore()
|
||||
|
||||
const router = useRouter()
|
||||
const projectRef = router.query.ref
|
||||
const orgSlug = router.query.slug
|
||||
const { ref, slug } = useParams()
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
|
||||
/**
|
||||
* Array of urls/routes that should be ignored
|
||||
@@ -43,14 +44,14 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
// check if current route is excempted from route validation check
|
||||
if (isExceptUrl()) return
|
||||
|
||||
if (orgsInitialized && orgSlug) {
|
||||
if (orgsInitialized && slug) {
|
||||
// Check validity of organization that user is trying to access
|
||||
const organizations = organizationsRef.current ?? []
|
||||
const isValidOrg = organizations.some((org) => org.slug === orgSlug)
|
||||
const isValidOrg = organizations.some((org) => org.slug === slug)
|
||||
|
||||
if (!isValidOrg) {
|
||||
ui.setNotification({ category: 'error', message: 'This organization does not exist' })
|
||||
router.push('/projects')
|
||||
router.push(navLayoutV2 ? `/org/${organizations[0].slug}` : '/projects')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -63,19 +64,43 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
// check if current route is excempted from route validation check
|
||||
if (isExceptUrl()) return
|
||||
|
||||
if (projectsInitialized && projectRef) {
|
||||
if (projectsInitialized && ref) {
|
||||
// Check validity of project that the user is trying to access
|
||||
const projects = projectsRef.current ?? []
|
||||
const isValidProject = projects.some((project) => project.ref === projectRef)
|
||||
const isValidProject = projects.some((project) => project.ref === ref)
|
||||
|
||||
if (!isValidProject) {
|
||||
ui.setNotification({ category: 'error', message: 'This project does not exist' })
|
||||
router.push('/projects')
|
||||
router.push(navLayoutV2 ? `/org/${organizations?.[0].slug}` : '/projects')
|
||||
return
|
||||
}
|
||||
}
|
||||
}, [projectsInitialized])
|
||||
|
||||
useEffect(() => {
|
||||
if (orgsInitialized && slug) {
|
||||
// Save organization slug to local storage
|
||||
const organizations = organizationsRef.current ?? []
|
||||
const organization = organizations.find((org) => org.slug === slug)
|
||||
if (organization) {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION, organization.slug)
|
||||
}
|
||||
}
|
||||
}, [slug, orgsInitialized])
|
||||
|
||||
useEffect(() => {
|
||||
if (projectsInitialized && ref) {
|
||||
// Save organization slug to local storage
|
||||
const projects = projectsRef.current ?? []
|
||||
const project = projects.find((project) => project.ref === ref)
|
||||
const organizationId = project?.organization_id
|
||||
const organization = organizations?.find((organization) => organization.id === organizationId)
|
||||
if (organization) {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION, organization.slug)
|
||||
}
|
||||
}
|
||||
}, [ref, projectsInitialized])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const ProjectCard = ({ project, rewriteHref }: ProjectCardProps) => {
|
||||
const isRestoring = project.status === PROJECT_STATUS.RESTORING
|
||||
|
||||
return (
|
||||
<li className="col-span-1">
|
||||
<li className="col-span-1 list-none">
|
||||
<CardButton
|
||||
linkHref={rewriteHref ? rewriteHref : `/project/${projectRef}`}
|
||||
title={
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useEffect, useState } from 'react'
|
||||
import { Collapsible, Form, IconChevronRight, Input, Toggle } from 'ui'
|
||||
|
||||
import { useParams } from 'common/hooks'
|
||||
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
|
||||
import {
|
||||
FormActions,
|
||||
FormPanel,
|
||||
@@ -18,9 +19,8 @@ import { invalidateOrganizationsQuery } from 'data/organizations/organizations-q
|
||||
import { useCheckPermissions, useFlag, useSelectedOrganization, useStore } from 'hooks'
|
||||
import { isResponseOk, patch } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import OrganizationDeletePanel from './OrganizationDeletePanel'
|
||||
import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold'
|
||||
import { Organization } from 'types'
|
||||
import OrganizationDeletePanel from './OrganizationDeletePanel'
|
||||
|
||||
const GeneralSettings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -82,7 +82,7 @@ const InfrastructureInfo = () => {
|
||||
label="Current version"
|
||||
actions={[
|
||||
isOnLatestVersion && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Root key="tooltip-latest" delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Badge color="green" className="mr-1">
|
||||
Latest
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSignOut } from 'lib/auth'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { SidebarSection } from './AccountLayout.types'
|
||||
import WithSidebar from './WithSidebar'
|
||||
import SettingsLayout from '../SettingsLayout/SettingsLayout'
|
||||
|
||||
export interface AccountLayoutProps {
|
||||
title: string
|
||||
@@ -23,12 +24,12 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
|
||||
|
||||
const signOut = useSignOut()
|
||||
const onClickLogout = async () => {
|
||||
await signOut()
|
||||
|
||||
await router.push('/sign-in')
|
||||
}
|
||||
|
||||
@@ -126,6 +127,10 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
|
||||
: []),
|
||||
]
|
||||
|
||||
if (navLayoutV2) {
|
||||
return <SettingsLayout>{children}</SettingsLayout>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -137,7 +142,12 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren<Accou
|
||||
style={{ height: maxHeight, maxHeight }}
|
||||
className="flex flex-col flex-1 w-full overflow-y-auto"
|
||||
>
|
||||
<WithSidebar title={title} breadcrumbs={breadcrumbs} sections={sectionsWithHeaders}>
|
||||
<WithSidebar
|
||||
hideSidebar={navLayoutV2}
|
||||
title={title}
|
||||
breadcrumbs={breadcrumbs}
|
||||
sections={sectionsWithHeaders}
|
||||
>
|
||||
{children}
|
||||
</WithSidebar>
|
||||
</main>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useFlag } from 'hooks'
|
||||
import { isUndefined } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { Menu, IconArrowUpRight, Badge, IconLogOut } from 'ui'
|
||||
import { useFlag } from 'hooks'
|
||||
import { Badge, IconArrowUpRight, IconLogOut, Menu } from 'ui'
|
||||
import LayoutHeader from '../ProjectLayout/LayoutHeader'
|
||||
import { SidebarLink, SidebarSection } from './AccountLayout.types'
|
||||
|
||||
interface Props {
|
||||
interface WithSidebarProps {
|
||||
title: string
|
||||
breadcrumbs: any[]
|
||||
sections: SidebarSection[]
|
||||
@@ -27,7 +27,7 @@ The information hierarchy for WithSidebar is:
|
||||
SidebarItem
|
||||
SidebarLink
|
||||
*/
|
||||
const WithSidebar: FC<Props> = ({
|
||||
const WithSidebar = ({
|
||||
title,
|
||||
header,
|
||||
breadcrumbs = [],
|
||||
@@ -37,9 +37,10 @@ const WithSidebar: FC<Props> = ({
|
||||
subitemsParentKey,
|
||||
hideSidebar = false,
|
||||
customSidebarContent,
|
||||
}) => {
|
||||
}: WithSidebarProps) => {
|
||||
const noContent = !sections && !customSidebarContent
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
|
||||
|
||||
return (
|
||||
@@ -89,7 +90,7 @@ const WithSidebar: FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-1 flex-col">
|
||||
<LayoutHeader breadcrumbs={breadcrumbs} />
|
||||
{!navLayoutV2 && <LayoutHeader breadcrumbs={breadcrumbs} />}
|
||||
<div className="flex-1 flex-grow overflow-auto">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import FeedbackDropdown from '../ProjectLayout/LayoutHeader/FeedbackDropdown'
|
||||
import HelpPopover from '../ProjectLayout/LayoutHeader/HelpPopover'
|
||||
import NotificationsPopover from '../ProjectLayout/LayoutHeader/NotificationsPopover'
|
||||
import OrganizationDropdown from './OrganizationDropdown'
|
||||
import ProjectDropdown from './ProjectDropdown'
|
||||
import SettingsButton from './SettingsButton'
|
||||
import UserSettingsDropdown from './UserSettingsDropdown'
|
||||
|
||||
const AppHeader = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const organization = useSelectedOrganization()
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 py-1 bg-scale-200 border-b">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href={organization !== undefined ? `/org/${organization?.slug}` : '/'}>
|
||||
<a className="block">
|
||||
<img
|
||||
src={`${router.basePath}/img/supabase-logo.svg`}
|
||||
alt="Supabase"
|
||||
className="mx-auto h-[40px] w-6 cursor-pointer rounded"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<OrganizationDropdown />
|
||||
{ref !== undefined && <ProjectDropdown />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<FeedbackDropdown alt />
|
||||
<NotificationsPopover alt />
|
||||
<HelpPopover alt />
|
||||
<SettingsButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<UserSettingsDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppHeader
|
||||
@@ -0,0 +1,32 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
import AppHeader from './AppHeader'
|
||||
import { useFlag } from 'hooks'
|
||||
|
||||
const AppLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
|
||||
const appHeaderHeight = navLayoutV2 ? 49 : 0
|
||||
const incidentBannerHeight = 44
|
||||
|
||||
return (
|
||||
<div>
|
||||
{navLayoutV2 && <AppHeader />}
|
||||
<div
|
||||
style={{
|
||||
height: ongoingIncident
|
||||
? `calc(100vh - ${incidentBannerHeight}px - ${appHeaderHeight}px)`
|
||||
: `calc(100vh - ${appHeaderHeight}px)`,
|
||||
maxHeight: ongoingIncident
|
||||
? `calc(100vh - ${incidentBannerHeight}px - ${appHeaderHeight}px)`
|
||||
: `calc(100vh - ${appHeaderHeight}px)`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppLayout
|
||||
@@ -0,0 +1,19 @@
|
||||
import Link from 'next/link'
|
||||
import { IconExternalLink } from 'ui'
|
||||
|
||||
const IncidentBanner = () => {
|
||||
return (
|
||||
<Link href="https://status.supabase.com">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<div className="flex cursor-pointer items-center justify-center space-x-2 bg-green-900 py-3 text-scale-400 transition hover:bg-green-1000 dark:text-scale-1200">
|
||||
<p className="text-sm font-medium">
|
||||
We are currently investigating a technical issue, follow status.supabase.com for updates
|
||||
</p>
|
||||
<IconExternalLink size={16} strokeWidth={2} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default IncidentBanner
|
||||
@@ -0,0 +1,129 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useRef, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
IconCheck,
|
||||
IconCode,
|
||||
IconPlus,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useFlag, useSelectedOrganization } from 'hooks'
|
||||
|
||||
const OrganizationDropdown = () => {
|
||||
const router = useRouter()
|
||||
const orgCreationV2 = useFlag('orgcreationv2')
|
||||
const orgNameRef = useRef<HTMLAnchorElement>(null)
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const { data: organizations, isLoading: isLoadingOrganizations } = useOrganizationsQuery()
|
||||
|
||||
const slug = selectedOrganization?.slug
|
||||
const orgName = selectedOrganization?.name
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
const { data: subscription, isSuccess } = useOrgSubscriptionQuery(
|
||||
{ orgSlug: slug },
|
||||
{ enabled: isOrgBilling }
|
||||
)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const popoverOffset = (orgNameRef.current?.offsetWidth ?? 0) + 12
|
||||
|
||||
if (isLoadingOrganizations) {
|
||||
return <ShimmeringLoader className="w-[90px]" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2 px-2">
|
||||
<Link passHref href={slug ? `/org/${slug}` : '/'}>
|
||||
<a ref={orgNameRef} className="flex items-center space-x-2">
|
||||
<p className="text-sm">{orgName}</p>
|
||||
{isSuccess && <Badge color="scale">{subscription?.plan.name}</Badge>}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={<IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} />}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0"
|
||||
side="bottom"
|
||||
align="start"
|
||||
style={{ marginLeft: `-${popoverOffset}px` }}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Find organization..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No organizations found</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{organizations?.map((org) => {
|
||||
const href = router.pathname.includes('[slug]')
|
||||
? router.pathname.replace('[slug]', org.slug)
|
||||
: router.pathname.includes('[ref]/settings')
|
||||
? `/org/${org.slug}/general`
|
||||
: `/org/${org.slug}`
|
||||
return (
|
||||
<Link passHref href={href} key={org.slug}>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
value={org.name}
|
||||
className="cursor-pointer w-full flex items-center justify-between"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
router.push(href)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<a>
|
||||
{org.name}
|
||||
{org.slug === slug && <IconCheck />}
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</CommandGroup_Shadcn_>
|
||||
<CommandGroup_Shadcn_ className="border-t">
|
||||
<Link passHref href={orgCreationV2 ? `/new-with-subscription` : `/new`}>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
className="cursor-pointer flex items-center space-x-2 w-full"
|
||||
onSelect={(e) => {
|
||||
setOpen(false)
|
||||
router.push(orgCreationV2 ? `/new-with-subscription` : `/new`)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<a>
|
||||
<IconPlus size={14} strokeWidth={1.5} />
|
||||
<p>New organization</p>
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrganizationDropdown
|
||||
@@ -0,0 +1,200 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import { useRef, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
IconCheck,
|
||||
IconCode,
|
||||
IconPlus,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { Organization, Project } from 'types'
|
||||
import { useParams } from 'common'
|
||||
|
||||
// [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,
|
||||
setOpen,
|
||||
}: {
|
||||
project: Project
|
||||
organization?: Organization
|
||||
setOpen: (value: boolean) => void
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
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}>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
key={project.ref}
|
||||
value={project.name}
|
||||
className="cursor-pointer w-full flex items-center justify-between"
|
||||
onSelect={() => {
|
||||
router.push(href)
|
||||
setOpen(false)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<a>
|
||||
{project.name}
|
||||
{project.ref === ref && <IconCheck />}
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const ProjectDropdown = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const projectDetails = useSelectedProject()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const projectNameRef = useRef<HTMLAnchorElement>(null)
|
||||
const { data: allProjects, isLoading: isLoadingProjects } = useProjectsQuery()
|
||||
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
const { data: subscription, isSuccess } = useProjectSubscriptionV2Query(
|
||||
{ projectRef: ref },
|
||||
{ enabled: !isOrgBilling }
|
||||
)
|
||||
const projects = allProjects
|
||||
?.filter((x) => x.status !== PROJECT_STATUS.INACTIVE)
|
||||
.filter((x) => x.organization_id === selectedOrganization?.id)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const selectedProject = projectDetails || projects?.find((project) => project.ref === ref)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const popoverOffset = (projectNameRef.current?.offsetWidth ?? 0) + 12
|
||||
|
||||
if (isLoadingProjects) {
|
||||
return <ShimmeringLoader className="w-[90px]" />
|
||||
}
|
||||
|
||||
return IS_PLATFORM ? (
|
||||
<div className="flex items-center space-x-2 px-2">
|
||||
<Link href={`/project/${selectedProject?.ref}`}>
|
||||
<a ref={projectNameRef} className="flex items-center space-x-2">
|
||||
<p className="text-sm">{selectedProject?.name}</p>
|
||||
{isSuccess && !isOrgBilling && <Badge color="slate">{subscription?.plan.name}</Badge>}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={<IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} />}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0"
|
||||
side="bottom"
|
||||
align="start"
|
||||
style={{ marginLeft: `-${popoverOffset}px` }}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Find project..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No projects found</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{projects?.map((project) => (
|
||||
<ProjectLink
|
||||
key={project.ref}
|
||||
project={project}
|
||||
organization={selectedOrganization}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
<CommandGroup_Shadcn_ className="border-t">
|
||||
<Link
|
||||
passHref
|
||||
href={`/new/${selectedOrganization?.slug}`}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
className="cursor-pointer flex items-center space-x-2 w-full"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
router.push(`/new/${selectedOrganization?.slug}`)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<a>
|
||||
<IconPlus size={14} strokeWidth={1.5} />
|
||||
<p>New project</p>
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</div>
|
||||
) : (
|
||||
<Button type="text">
|
||||
<span className="text-sm">{selectedProject?.name}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectDropdown
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import Link from 'next/link'
|
||||
import { IconSettings } from 'ui'
|
||||
|
||||
const SettingsButton = () => {
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const slug = selectedOrganization?.slug
|
||||
|
||||
return (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Link href={slug ? `/org/${slug}/general` : '/'} passHref>
|
||||
<Tooltip.Trigger type="button" asChild className="px-1">
|
||||
<a>
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-scale-1100" />
|
||||
</a>
|
||||
</Tooltip.Trigger>
|
||||
</Link>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">Settings</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsButton
|
||||
@@ -0,0 +1,120 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
DropdownMenuCheckboxItem_Shadcn_,
|
||||
DropdownMenuContent_Shadcn_,
|
||||
DropdownMenuGroup_Shadcn_,
|
||||
DropdownMenuItem_Shadcn_,
|
||||
DropdownMenuLabel_Shadcn_,
|
||||
DropdownMenuRadioGroup_Shadcn_,
|
||||
DropdownMenuRadioItem_Shadcn_,
|
||||
DropdownMenuSeparator_Shadcn_,
|
||||
DropdownMenuShortcut_Shadcn_,
|
||||
DropdownMenuSubContent_Shadcn_,
|
||||
DropdownMenuSubTrigger_Shadcn_,
|
||||
DropdownMenuSub_Shadcn_,
|
||||
DropdownMenuTrigger_Shadcn_,
|
||||
DropdownMenu_Shadcn_,
|
||||
IconCheck,
|
||||
IconUser,
|
||||
useCommandMenu,
|
||||
} from 'ui'
|
||||
|
||||
import { useTheme } from 'common'
|
||||
import { useSignOut } from 'lib/auth'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useState } from 'react'
|
||||
|
||||
const UserSettingsDropdown = () => {
|
||||
const signOut = useSignOut()
|
||||
const router = useRouter()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { profile } = useProfile()
|
||||
const { setIsOpen: setCommandMenuOpen } = useCommandMenu()
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
|
||||
const onClickLogout = async () => {
|
||||
await signOut()
|
||||
await router.push('/sign-in')
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
||||
<DropdownMenuTrigger_Shadcn_ className="flex items-center justify-center border font-bold rounded-full h-7 w-7 text-scale-1100 bg-surface-100">
|
||||
{profile?.first_name ? profile?.first_name?.[0] : <IconUser size={14} strokeWidth={2} />}
|
||||
</DropdownMenuTrigger_Shadcn_>
|
||||
<DropdownMenuContent_Shadcn_ align="end" className="w-60">
|
||||
<DropdownMenuGroup_Shadcn_>
|
||||
<div key="profile" className="px-2 py-1.5">
|
||||
<p className="text-sm text-scale-1200">
|
||||
{profile?.first_name} {profile?.last_name}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000 truncate">{profile?.primary_email}</p>
|
||||
</div>
|
||||
|
||||
<DropdownMenuSeparator_Shadcn_ />
|
||||
<Link passHref href="/account/me">
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="cursor-pointer"
|
||||
onSelect={() => {
|
||||
router.push('/account/me')
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
asChild
|
||||
>
|
||||
<a>Preferences</a>
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</Link>
|
||||
<Link passHref href="/account/tokens">
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="cursor-pointer"
|
||||
onSelect={() => {
|
||||
router.push('/account/tokens')
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
asChild
|
||||
>
|
||||
<a>Access tokens</a>
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</Link>
|
||||
<DropdownMenuSeparator_Shadcn_ />
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="cursor-pointer"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
setCommandMenuOpen(true)
|
||||
}}
|
||||
>
|
||||
<span>Command menu</span>
|
||||
<DropdownMenuShortcut_Shadcn_>⌘K</DropdownMenuShortcut_Shadcn_>
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
<DropdownMenuSeparator_Shadcn_ />
|
||||
<DropdownMenuLabel_Shadcn_>Theme</DropdownMenuLabel_Shadcn_>
|
||||
<DropdownMenuRadioGroup_Shadcn_
|
||||
value={isDarkMode ? 'dark' : 'light'}
|
||||
onValueChange={(x) => {
|
||||
const dark = x === 'dark'
|
||||
toggleTheme(Boolean(dark))
|
||||
}}
|
||||
>
|
||||
<DropdownMenuRadioItem_Shadcn_ value={'dark'}>Dark</DropdownMenuRadioItem_Shadcn_>
|
||||
<DropdownMenuRadioItem_Shadcn_ value={'light'}>Light</DropdownMenuRadioItem_Shadcn_>
|
||||
</DropdownMenuRadioGroup_Shadcn_>
|
||||
|
||||
<DropdownMenuSeparator_Shadcn_ />
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
onClickLogout()
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<span>Log out</span>
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</DropdownMenuGroup_Shadcn_>
|
||||
</DropdownMenuContent_Shadcn_>
|
||||
</DropdownMenu_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserSettingsDropdown
|
||||
@@ -5,16 +5,23 @@ import { PropsWithChildren } from 'react'
|
||||
import { Tabs } from 'ui'
|
||||
import { AccountLayout } from './'
|
||||
import { ScaffoldContainer, ScaffoldDivider, ScaffoldHeader, ScaffoldTitle } from './Scaffold'
|
||||
import SettingsLayout from './SettingsLayout/SettingsLayout'
|
||||
|
||||
const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const router = useRouter()
|
||||
const { slug } = useParams()
|
||||
const id = router.asPath.split('/').at(-1)?.split('?')[0]?.split('#')[0]
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const showOAuthApps = useFlag('oauthApps')
|
||||
const showAuditLogs = useFlag('auditLogs')
|
||||
const showIntegrationsV2 = useFlag('integrationsV2')
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
|
||||
if (navLayoutV2) {
|
||||
return <SettingsLayout>{children}</SettingsLayout>
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountLayout
|
||||
|
||||
+33
-18
@@ -1,8 +1,19 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, IconMessageCircle, Popover } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
IconMessageCircle,
|
||||
Popover,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
import FeedbackWidget from './FeedbackWidget'
|
||||
|
||||
const FeedbackDropdown = () => {
|
||||
interface FeedbackDropdownProps {
|
||||
alt?: boolean
|
||||
}
|
||||
|
||||
const FeedbackDropdown = ({ alt = false }: FeedbackDropdownProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [screenshot, setScreenshot] = useState<string>()
|
||||
@@ -18,16 +29,29 @@ const FeedbackDropdown = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
<Popover_Shadcn_
|
||||
modal={false}
|
||||
open={isOpen}
|
||||
onOpenChange={(e) => {
|
||||
setIsOpen(e)
|
||||
if (!e) setScreenshot(undefined)
|
||||
}}
|
||||
size="content"
|
||||
side="bottom"
|
||||
align="end"
|
||||
overlay={
|
||||
>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
asChild
|
||||
onClick={onOpen}
|
||||
type="default"
|
||||
icon={
|
||||
alt ? null : (
|
||||
<IconMessageCircle size={16} strokeWidth={1.5} className="text-scale-900" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className="hidden md:flex">Feedback</span>
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ side="bottom" align="end" className="w-full p-0">
|
||||
<FeedbackWidget
|
||||
onClose={onClose}
|
||||
feedback={feedback}
|
||||
@@ -35,17 +59,8 @@ const FeedbackDropdown = () => {
|
||||
screenshot={screenshot}
|
||||
setScreenshot={setScreenshot}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
asChild
|
||||
onClick={onOpen}
|
||||
type="default"
|
||||
icon={<IconMessageCircle size={16} strokeWidth={1.5} className="text-scale-900" />}
|
||||
>
|
||||
<span className="hidden md:flex">Feedback</span>
|
||||
</Button>
|
||||
</Popover>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+45
-31
@@ -2,7 +2,21 @@ import { toPng } from 'html-to-image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ChangeEvent, FC, useEffect, useRef, useState } from 'react'
|
||||
import { Button, Dropdown, IconCamera, IconImage, IconUpload, IconX, Input, Popover } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownMenuContent_Shadcn_,
|
||||
DropdownMenuItem_Shadcn_,
|
||||
DropdownMenuSeparator_Shadcn_,
|
||||
DropdownMenuTrigger_Shadcn_,
|
||||
DropdownMenu_Shadcn_,
|
||||
IconCamera,
|
||||
IconImage,
|
||||
IconUpload,
|
||||
IconX,
|
||||
Input,
|
||||
Popover,
|
||||
} from 'ui'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useSendFeedbackMutation } from 'data/feedback/feedback-send'
|
||||
@@ -125,13 +139,12 @@ const FeedbackWidget: FC<Props> = ({
|
||||
value={feedback}
|
||||
onChange={onFeedbackChange}
|
||||
/>
|
||||
<Popover.Separator />
|
||||
<div className="w-full h-px bg-border" />
|
||||
<div className="w-80 space-y-3 px-3 py-2 pb-4">
|
||||
<div className="flex justify-between space-x-2">
|
||||
<Button type="default" onClick={onClose} className="hover:border-gray-500">
|
||||
<Button type="default" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{screenshot !== undefined ? (
|
||||
<div
|
||||
@@ -143,7 +156,7 @@ const FeedbackWidget: FC<Props> = ({
|
||||
}}
|
||||
className="cursor-pointer rounded h-[26px] w-[30px] border border-scale-600 relative bg-cover bg-center bg-no-repeat"
|
||||
>
|
||||
<div
|
||||
<button
|
||||
className={[
|
||||
'cursor-pointer rounded-full bg-red-900 h-3 w-3',
|
||||
'flex items-center justify-center absolute -top-1 -right-1',
|
||||
@@ -154,42 +167,43 @@ const FeedbackWidget: FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<IconX size={8} strokeWidth={3} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Dropdown
|
||||
className="feedback-dropdown"
|
||||
size="small"
|
||||
overlay={[
|
||||
<Dropdown.Item
|
||||
<DropdownMenu_Shadcn_>
|
||||
<DropdownMenuTrigger_Shadcn_>
|
||||
<Button
|
||||
asChild
|
||||
type="default"
|
||||
disabled={isSavingScreenshot}
|
||||
loading={isSavingScreenshot}
|
||||
className="px-2 py-1.5"
|
||||
icon={<IconImage size={14} />}
|
||||
>
|
||||
<span></span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger_Shadcn_>
|
||||
<DropdownMenuContent_Shadcn_ side="bottom" align="end">
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="flex gap-2"
|
||||
key="upload-screenshot"
|
||||
icon={<IconUpload size={14} />}
|
||||
onClick={() => {
|
||||
onSelect={() => {
|
||||
if (uploadButtonRef.current) (uploadButtonRef.current as any).click()
|
||||
}}
|
||||
>
|
||||
<IconUpload size={14} />
|
||||
Upload screenshot
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Item
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
<DropdownMenuItem_Shadcn_
|
||||
className="flex gap-2"
|
||||
key="capture-screenshot"
|
||||
icon={<IconCamera size={14} />}
|
||||
onClick={() => captureScreenshot()}
|
||||
onSelect={() => captureScreenshot()}
|
||||
>
|
||||
<IconCamera size={14} />
|
||||
Capture screenshot
|
||||
</Dropdown.Item>,
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
asChild
|
||||
type="default"
|
||||
disabled={isSavingScreenshot}
|
||||
loading={isSavingScreenshot}
|
||||
className="px-2 py-1.5"
|
||||
icon={<IconImage size={14} />}
|
||||
>
|
||||
<span></span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</DropdownMenuContent_Shadcn_>
|
||||
</DropdownMenu_Shadcn_>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
|
||||
@@ -1,148 +1,172 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import SVG from 'react-inlinesvg'
|
||||
import {
|
||||
Button,
|
||||
IconActivity,
|
||||
IconBookOpen,
|
||||
IconHelpCircle,
|
||||
IconMail,
|
||||
IconMessageCircle,
|
||||
Popover,
|
||||
IconBookOpen,
|
||||
IconActivity,
|
||||
IconTool,
|
||||
Popover,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
import SVG from 'react-inlinesvg'
|
||||
interface HelpPopoverProps {
|
||||
alt?: boolean
|
||||
}
|
||||
|
||||
interface Props {}
|
||||
|
||||
const HelpPopover: FC<Props> = () => {
|
||||
const HelpPopover = ({ alt = false }: HelpPopoverProps) => {
|
||||
const router = useRouter()
|
||||
const projectRef = router.query.ref
|
||||
const supportUrl = `/support/new${projectRef ? `?ref=${projectRef}` : ''}`
|
||||
|
||||
return (
|
||||
<Popover
|
||||
size="content"
|
||||
align="end"
|
||||
side="bottom"
|
||||
sideOffset={8}
|
||||
overlay={
|
||||
<div className="my-4 w-[400px] space-y-4">
|
||||
<div className="my-5 space-y-4 px-5">
|
||||
<h5 className="text-scale-1200">Need help with your project?</h5>
|
||||
<p className="text-sm text-scale-900">
|
||||
For issues with your project hosted on supabase.com, or other inquiries about our
|
||||
hosted services.
|
||||
</p>
|
||||
<div className="space-x-1">
|
||||
<Link passHref href="https://supabase.com/docs/guides/platform/troubleshooting">
|
||||
<Button asChild type="default" icon={<IconTool />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Troubleshooting
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link passHref href="https://supabase.com/docs/">
|
||||
<Button asChild type="text" size="tiny" icon={<IconBookOpen />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Docs
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link passHref href="https://status.supabase.com/">
|
||||
<Button asChild type="text" size="tiny" icon={<IconActivity />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Supabase Status
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
<Popover_Shadcn_>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className="relative flex items-center">
|
||||
<Button
|
||||
type={alt ? 'text' : 'default'}
|
||||
className={alt ? 'px-1' : ''}
|
||||
icon={
|
||||
<IconHelpCircle
|
||||
size={alt ? 18 : 16}
|
||||
strokeWidth={1.5}
|
||||
className={alt ? 'text-scale-1100' : 'text-scale-900'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{!alt && <span>Help</span>}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
{alt ? (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'space-y-2 border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-xs text-scale-1200">Help</p>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
) : null}
|
||||
</Tooltip.Root>
|
||||
<PopoverContent_Shadcn_ className="w-[400px] space-y-4 p-0 py-5" align="end" side="bottom">
|
||||
<div className="mb-5 space-y-4 px-5">
|
||||
<h5 className="text-scale-1200">Need help with your project?</h5>
|
||||
<p className="text-sm text-scale-900">
|
||||
For issues with your project hosted on supabase.com, or other inquiries about our hosted
|
||||
services.
|
||||
</p>
|
||||
<div className="space-x-1">
|
||||
<Link passHref href="https://supabase.com/docs/guides/platform/troubleshooting">
|
||||
<Button asChild type="default" icon={<IconTool />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Troubleshooting
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link passHref href="https://supabase.com/docs/">
|
||||
<Button asChild type="text" size="tiny" icon={<IconBookOpen />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Docs
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link passHref href="https://status.supabase.com/">
|
||||
<Button asChild type="text" size="tiny" icon={<IconActivity />}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
Supabase Status
|
||||
</a>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="text-sm text-scale-900">
|
||||
Expected response time is based on your billing plan. Pro and Pay as You Go plans are
|
||||
prioritized.
|
||||
</p>
|
||||
<div>
|
||||
<Link passHref href={supportUrl}>
|
||||
<Button asChild type="default" icon={<IconMail />}>
|
||||
<a>Contact Support</a>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Popover.Separator />
|
||||
<div className="mb-4 space-y-2">
|
||||
<div className="mb-4 px-5">
|
||||
<h5 className={'mb-2'}>Reach out to the community</h5>
|
||||
|
||||
<p className="text-sm text-scale-900">
|
||||
Expected response time is based on your billing plan. Pro and Pay as You Go plans are
|
||||
prioritized.
|
||||
For other support, including questions on our client libraries, advice, or best
|
||||
practices.
|
||||
</p>
|
||||
<div>
|
||||
<Link passHref href={supportUrl}>
|
||||
<Button asChild type="default" icon={<IconMail />}>
|
||||
<a>Contact Support</a>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div
|
||||
className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md"
|
||||
style={{ background: '#404EED' }}
|
||||
>
|
||||
<a
|
||||
href="https://discord.supabase.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="dark block cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
className="absolute left-0 top-0 opacity-50"
|
||||
src={`${router.basePath}/img/support/discord-bg-small.jpg`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<Button
|
||||
type="secondary"
|
||||
icon={<SVG src={`${router.basePath}/img/discord-icon.svg`} className="h-4 w-4" />}
|
||||
>
|
||||
<span style={{ color: '#404EED' }}>Join Discord server</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Popover.Separator />
|
||||
<div className="mb-4 space-y-2">
|
||||
<div className="mb-4 px-5">
|
||||
<h5 className={'mb-2'}>Reach out to the community</h5>
|
||||
|
||||
<p className="text-sm text-scale-900">
|
||||
For other support, including questions on our client libraries, advice, or best
|
||||
practices.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div
|
||||
className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md"
|
||||
style={{ background: '#404EED' }}
|
||||
<div className="px-5">
|
||||
<div className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md">
|
||||
<a
|
||||
href="https://github.com/supabase/supabase/discussions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block cursor-pointer"
|
||||
>
|
||||
<a
|
||||
href="https://discord.supabase.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="dark block cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
className="absolute left-0 top-0 opacity-50"
|
||||
src={`${router.basePath}/img/support/discord-bg-small.jpg`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<Button
|
||||
type="secondary"
|
||||
icon={
|
||||
<SVG src={`${router.basePath}/img/discord-icon.svg`} className="h-4 w-4" />
|
||||
}
|
||||
>
|
||||
<span style={{ color: '#404EED' }}>Join Discord server</span>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<div className="relative space-y-2 overflow-hidden rounded px-5 py-4 pb-12 shadow-md">
|
||||
<a
|
||||
href="https://github.com/supabase/supabase/discussions"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
className="absolute left-0 top-0 opacity-50"
|
||||
src={`${router.basePath}/img/support/github-bg.jpg?v-1`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<Button type="secondary" icon={<IconMessageCircle />}>
|
||||
GitHub Discussions
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<Image
|
||||
className="absolute left-0 top-0 opacity-50"
|
||||
src={`${router.basePath}/img/support/github-bg.jpg?v-1`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt="discord illustration header"
|
||||
/>
|
||||
<Button type="secondary" icon={<IconMessageCircle />}>
|
||||
GitHub Discussions
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
asChild
|
||||
type="default"
|
||||
icon={<IconHelpCircle size={16} strokeWidth={1.5} className="text-scale-900" />}
|
||||
>
|
||||
<span>Help</span>
|
||||
</Button>
|
||||
</Popover>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+44
-13
@@ -12,15 +12,19 @@ import { Fragment, useState } from 'react'
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import { useNotificationsQuery } from 'data/notifications/notifications-query'
|
||||
import { getProjectDetail } from 'data/projects/project-detail-query'
|
||||
import { invalidateProjectsQuery, setProjectPostgrestStatus } from 'data/projects/projects-query'
|
||||
import { setProjectPostgrestStatus } from 'data/projects/projects-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { delete_, patch, post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { Project } from 'types'
|
||||
import { Alert, Button, IconArrowRight, IconBell, Popover } from 'ui'
|
||||
import { Alert, Button, IconArrowRight, IconBell, IconInbox, Popover } from 'ui'
|
||||
import NotificationRow from './NotificationRow'
|
||||
|
||||
const NotificationsPopover = () => {
|
||||
interface NotificationsPopoverProps {
|
||||
alt?: boolean
|
||||
}
|
||||
|
||||
const NotificationsPopover = ({ alt = false }: NotificationsPopoverProps) => {
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const { meta, ui } = useStore()
|
||||
@@ -35,9 +39,10 @@ const NotificationsPopover = () => {
|
||||
|
||||
if (!notifications) return <></>
|
||||
|
||||
const hasNewNotifications = notifications?.some(
|
||||
const newNotifications = notifications?.filter(
|
||||
(notification) => notification.notification_status === NotificationStatus.New
|
||||
)
|
||||
const hasNewNotifications = newNotifications.length > 0
|
||||
|
||||
const onOpenChange = async (open: boolean) => {
|
||||
// TODO(alaister): move this to a mutation
|
||||
@@ -232,21 +237,47 @@ const NotificationsPopover = () => {
|
||||
>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className="relative flex">
|
||||
<div className="relative flex items-center">
|
||||
{hasNewNotifications && (
|
||||
<>
|
||||
{alt ? null : (
|
||||
<div className="absolute -top-1 -right-1 z-50 flex h-3 w-3 items-center justify-center">
|
||||
<div className="h-full w-full animate-ping rounded-full bg-green-800 opacity-60"></div>
|
||||
<div className="z-60 absolute top-0 right-0 h-full w-full rounded-full bg-green-900 opacity-80"></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
asChild
|
||||
id="notification-button"
|
||||
type="default"
|
||||
icon={<IconBell size={16} strokeWidth={1.5} className="text-scale-1200" />}
|
||||
type={alt ? 'text' : 'default'}
|
||||
className={alt ? 'px-1' : ''}
|
||||
icon={
|
||||
hasNewNotifications ? (
|
||||
<div className="-mr-3.5 z-10 h-4 w-4 flex items-center justify-center rounded-full bg-white">
|
||||
<p className="text-xs text-scale-100">{newNotifications.length}</p>
|
||||
</div>
|
||||
) : alt ? (
|
||||
<IconInbox size={18} strokeWidth={1.5} className="text-scale-1100" />
|
||||
) : (
|
||||
<IconBell size={16} strokeWidth={1.5} className="text-scale-1200" />
|
||||
)
|
||||
}
|
||||
iconRight={
|
||||
hasNewNotifications ? (
|
||||
<>
|
||||
{alt ? (
|
||||
<IconInbox size={18} strokeWidth={1.5} className="text-scale-1100" />
|
||||
) : (
|
||||
<IconBell size={16} strokeWidth={1.5} className="text-scale-1200" />
|
||||
)}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<span></span>
|
||||
</Button>
|
||||
{hasNewNotifications && (
|
||||
<div className="absolute -top-1 -right-1 z-50 flex h-3 w-3 items-center justify-center">
|
||||
<div className="h-full w-full animate-ping rounded-full bg-green-800 opacity-60"></div>
|
||||
<div className="z-60 absolute top-0 right-0 h-full w-full rounded-full bg-green-900 opacity-80"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ParsedUrlQuery } from 'querystring'
|
||||
import { Button, Dropdown, IconPlus, Popover } from 'ui'
|
||||
import { Badge, Button, Dropdown, IconCode, IconPlus, Popover } from 'ui'
|
||||
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { Organization, Project } from 'types'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
|
||||
// [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
|
||||
@@ -71,13 +73,23 @@ const ProjectLink = ({
|
||||
)
|
||||
}
|
||||
|
||||
const ProjectDropdown = () => {
|
||||
const ProjectDropdown = ({ alt }: { alt?: boolean }) => {
|
||||
const selectedProject = useSelectedProject()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const { data: allProjects } = useProjectsQuery()
|
||||
const { data: allProjects, isLoading: isLoadingProjects } = useProjectsQuery()
|
||||
const { data: allOrganizations } = useOrganizationsQuery()
|
||||
const selectedOrganizationSlug = selectedOrganization?.slug
|
||||
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
const { data: subscription, isSuccess } = useProjectSubscriptionV2Query(
|
||||
{ projectRef: selectedProject?.ref },
|
||||
{ enabled: alt && !isOrgBilling }
|
||||
)
|
||||
|
||||
if (isLoadingProjects && alt) {
|
||||
return <ShimmeringLoader className="w-[90px]" />
|
||||
}
|
||||
|
||||
return IS_PLATFORM ? (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
@@ -100,13 +112,23 @@ const ProjectDropdown = () => {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button asChild type="text" size="tiny" className="my-1">
|
||||
<span>{selectedProject?.name}</span>
|
||||
<Button
|
||||
type="text"
|
||||
iconRight={
|
||||
alt ? <IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} /> : null
|
||||
}
|
||||
>
|
||||
<span className="text-sm">{selectedProject?.name}</span>
|
||||
{alt && isSuccess && !isOrgBilling && (
|
||||
<Badge color="slate" className="ml-2">
|
||||
{subscription?.plan.name}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button asChild type="text" size="tiny">
|
||||
<span>{selectedProject?.name}</span>
|
||||
<Button type="text">
|
||||
<span className="text-sm">{selectedProject?.name}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { isUndefined } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC } from 'react'
|
||||
|
||||
import { useTheme } from 'common'
|
||||
import { useParams } from 'common/hooks'
|
||||
@@ -27,15 +26,13 @@ import {
|
||||
} from './NavigationBar.utils'
|
||||
import NavigationIconButton from './NavigationIconButton'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const NavigationBar: FC<Props> = ({}) => {
|
||||
const NavigationBar = ({}) => {
|
||||
const router = useRouter()
|
||||
const { isDarkMode, toggleTheme } = useTheme()
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
|
||||
const activeRoute = router.pathname.split('/')[3]
|
||||
const toolRoutes = generateToolRoutes(projectRef, project)
|
||||
@@ -46,22 +43,23 @@ const NavigationBar: FC<Props> = ({}) => {
|
||||
const { setIsOpen } = useCommandMenu()
|
||||
return (
|
||||
<div
|
||||
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
|
||||
className={[
|
||||
'flex w-14 flex-col justify-between overflow-y-hidden p-2',
|
||||
'border-r bg-body border-scale-500',
|
||||
].join(' ')}
|
||||
>
|
||||
<ul className="flex flex-col space-y-2">
|
||||
<Link href="/projects">
|
||||
<a className="block">
|
||||
<img
|
||||
src={`${router.basePath}/img/supabase-logo.svg`}
|
||||
alt="Supabase"
|
||||
className="mx-auto h-[40px] w-6 cursor-pointer rounded"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
{!navLayoutV2 && (
|
||||
<Link href="/projects">
|
||||
<a className="block">
|
||||
<img
|
||||
src={`${router.basePath}/img/supabase-logo.svg`}
|
||||
alt="Supabase"
|
||||
className="mx-auto h-[40px] w-6 cursor-pointer rounded"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<NavigationIconButton
|
||||
isActive={isUndefined(activeRoute) && !isUndefined(router.query.ref)}
|
||||
route={{
|
||||
@@ -71,7 +69,7 @@ const NavigationBar: FC<Props> = ({}) => {
|
||||
link: `/project/${projectRef}`,
|
||||
}}
|
||||
/>
|
||||
<div className="bg-scale-500 h-px w-full"></div>
|
||||
<div className="bg-scale-500 h-px w-full" />
|
||||
{toolRoutes.map((route) => (
|
||||
<NavigationIconButton
|
||||
key={route.key}
|
||||
@@ -97,77 +95,82 @@ const NavigationBar: FC<Props> = ({}) => {
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<ul className="flex flex-col space-y-4 items-center">
|
||||
{IS_PLATFORM && showCmdkHelper && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="border-none"
|
||||
>
|
||||
<div className="py-1">
|
||||
<IconSearch size={18} strokeWidth={2} className="text-scale-900" />
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="right">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200 flex items-center space-x-1',
|
||||
].join(' ')}
|
||||
{!navLayoutV2 && (
|
||||
<ul className="flex flex-col space-y-4 items-center">
|
||||
{IS_PLATFORM && showCmdkHelper && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="border-none"
|
||||
>
|
||||
{os === 'macos' ? (
|
||||
<IconCommand size={11.5} strokeWidth={1.5} className="text-scale-1200" />
|
||||
) : (
|
||||
<p className="text-xs">CTRL</p>
|
||||
)}
|
||||
<p className="text-xs">K</p>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
<Dropdown
|
||||
side="right"
|
||||
align="start"
|
||||
overlay={
|
||||
<>
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<Link href="/account/me">
|
||||
<Dropdown.Item key="header" icon={<IconSettings size={14} strokeWidth={1.5} />}>
|
||||
Account Preferences
|
||||
</Dropdown.Item>
|
||||
</Link>
|
||||
<Dropdown.Separator />
|
||||
</>
|
||||
)}
|
||||
<Dropdown.Label>Theme</Dropdown.Label>
|
||||
<Dropdown.RadioGroup
|
||||
key="theme"
|
||||
value={isDarkMode ? 'dark' : 'light'}
|
||||
onChange={(e: any) => toggleTheme(e === 'dark')}
|
||||
>
|
||||
{/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */}
|
||||
{/* <Dropdown.Radio value="system">System default</Dropdown.Radio> */}
|
||||
<Dropdown.Radio value="dark">Dark</Dropdown.Radio>
|
||||
<Dropdown.Radio value="light">Light</Dropdown.Radio>
|
||||
</Dropdown.RadioGroup>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button asChild type="text" size="tiny">
|
||||
<span className="py-1 h-10 border-none">
|
||||
<IconUser size={18} strokeWidth={2} className="text-scale-900" />
|
||||
</span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</ul>
|
||||
<div className="py-1">
|
||||
<IconSearch size={18} strokeWidth={2} className="text-scale-900" />
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="right">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200 flex items-center space-x-1',
|
||||
].join(' ')}
|
||||
>
|
||||
{os === 'macos' ? (
|
||||
<IconCommand size={11.5} strokeWidth={1.5} className="text-scale-1200" />
|
||||
) : (
|
||||
<p className="text-xs">CTRL</p>
|
||||
)}
|
||||
<p className="text-xs">K</p>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
<Dropdown
|
||||
side="right"
|
||||
align="start"
|
||||
overlay={
|
||||
<>
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<Link href="/account/me">
|
||||
<Dropdown.Item
|
||||
key="header"
|
||||
icon={<IconSettings size={14} strokeWidth={1.5} />}
|
||||
>
|
||||
Account Preferences
|
||||
</Dropdown.Item>
|
||||
</Link>
|
||||
<Dropdown.Separator />
|
||||
</>
|
||||
)}
|
||||
<Dropdown.Label>Theme</Dropdown.Label>
|
||||
<Dropdown.RadioGroup
|
||||
key="theme"
|
||||
value={isDarkMode ? 'dark' : 'light'}
|
||||
onChange={(e: any) => toggleTheme(e === 'dark')}
|
||||
>
|
||||
{/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */}
|
||||
{/* <Dropdown.Radio value="system">System default</Dropdown.Radio> */}
|
||||
<Dropdown.Radio value="dark">Dark</Dropdown.Radio>
|
||||
<Dropdown.Radio value="light">Light</Dropdown.Radio>
|
||||
</Dropdown.RadioGroup>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button asChild type="text" size="tiny">
|
||||
<span className="py-1 h-10 border-none">
|
||||
<IconUser size={18} strokeWidth={2} className="text-scale-900" />
|
||||
</span>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { useFlag } from 'hooks'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
interface Props {
|
||||
interface ProductMenuBarProps {
|
||||
title: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const ProductMenuBar: FC<Props> = ({ title, children }) => {
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
|
||||
|
||||
const ProductMenuBar = ({ title, children }: PropsWithChildren<ProductMenuBarProps>) => {
|
||||
return (
|
||||
<div
|
||||
style={{ height: maxHeight, maxHeight }}
|
||||
className={[
|
||||
'hide-scrollbar flex w-64 flex-col border-r', // Layout
|
||||
'bg-scale-200', // Light mode
|
||||
@@ -25,7 +19,9 @@ const ProductMenuBar: FC<Props> = ({ title, children }) => {
|
||||
>
|
||||
<h4 className="text-lg">{title}</h4>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto">{children}</div>
|
||||
<div className="flex-grow overflow-y-auto" style={{ maxHeight: 'calc(100vh - 96px)' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ProjectContextProvider } from './ProjectContext'
|
||||
import ProjectPausedState from './ProjectPausedState'
|
||||
import RestoringState from './RestoringState'
|
||||
import UpgradingState from './UpgradingState'
|
||||
import AppLayout from '../AppLayout/AppLayout'
|
||||
|
||||
// [Joshen] This is temporary while we unblock users from managing their project
|
||||
// if their project is not responding well for any reason. Eventually needs a bit of an overhaul
|
||||
@@ -59,61 +60,61 @@ const ProjectLayout = ({
|
||||
}: PropsWithChildren<ProjectLayoutProps>) => {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef } = useParams()
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const selectedProject = useSelectedProject()
|
||||
const projectName = selectedProject?.name
|
||||
const organizationName = selectedOrganization?.name
|
||||
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
|
||||
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
|
||||
const ignorePausedState =
|
||||
router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings')
|
||||
const showPausedState = isPaused && !ignorePausedState
|
||||
|
||||
return (
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
<Head>
|
||||
<title>
|
||||
{title
|
||||
? `${title} | Supabase`
|
||||
: selectedTable
|
||||
? `${selectedTable} | ${projectName} | ${organizationName} | Supabase`
|
||||
: projectName
|
||||
? `${projectName} | ${organizationName} | Supabase`
|
||||
: organizationName
|
||||
? `${organizationName} | Supabase`
|
||||
: 'Supabase'}
|
||||
</title>
|
||||
<meta name="description" content="Supabase Studio" />
|
||||
</Head>
|
||||
<div className="flex h-full">
|
||||
{/* Left-most navigation side bar to access products */}
|
||||
{!hideIconBar && <NavigationBar />}
|
||||
<AppLayout>
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
<Head>
|
||||
<title>
|
||||
{title
|
||||
? `${title} | Supabase`
|
||||
: selectedTable
|
||||
? `${selectedTable} | ${projectName} | ${organizationName} | Supabase`
|
||||
: projectName
|
||||
? `${projectName} | ${organizationName} | Supabase`
|
||||
: organizationName
|
||||
? `${organizationName} | Supabase`
|
||||
: 'Supabase'}
|
||||
</title>
|
||||
<meta name="description" content="Supabase Studio" />
|
||||
</Head>
|
||||
<div className="flex h-full">
|
||||
{/* Left-most navigation side bar to access products */}
|
||||
{!hideIconBar && <NavigationBar />}
|
||||
|
||||
{/* Product menu bar */}
|
||||
{!showPausedState && (
|
||||
<MenuBarWrapper isLoading={isLoading} productMenu={productMenu}>
|
||||
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
|
||||
</MenuBarWrapper>
|
||||
)}
|
||||
|
||||
<main
|
||||
className="flex flex-col flex-1 w-full overflow-x-hidden"
|
||||
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
|
||||
>
|
||||
{!hideHeader && <LayoutHeader />}
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
<ProjectPausedState product={product} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
|
||||
{/* Product menu bar */}
|
||||
{!showPausedState && (
|
||||
<MenuBarWrapper isLoading={isLoading} productMenu={productMenu}>
|
||||
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
|
||||
</MenuBarWrapper>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</ProjectContextProvider>
|
||||
|
||||
<main className="flex flex-col flex-1 w-full overflow-x-hidden">
|
||||
{!navLayoutV2 && !hideHeader && <LayoutHeader />}
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
<ProjectPausedState product={product} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ContentWrapper isLoading={isLoading}>{children}</ContentWrapper>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</ProjectContextProvider>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -210,44 +211,44 @@ export const ProjectLayoutNonBlocking = ({
|
||||
const selectedProject = useSelectedProject()
|
||||
const router = useRouter()
|
||||
const { ref: projectRef } = useParams()
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
|
||||
const ignorePausedState =
|
||||
router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings')
|
||||
const showPausedState = isPaused && !ignorePausedState
|
||||
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
|
||||
return (
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
<Head>
|
||||
<title>{title ? `${title} | Supabase` : 'Supabase'}</title>
|
||||
<meta name="description" content="Supabase Studio" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="flex h-full">
|
||||
{/* Left-most navigation side bar to access products */}
|
||||
{!hideIconBar && <NavigationBar />}
|
||||
<AppLayout>
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
<Head>
|
||||
<title>{title ? `${title} | Supabase` : 'Supabase'}</title>
|
||||
<meta name="description" content="Supabase Studio" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="flex h-full">
|
||||
{/* Left-most navigation side bar to access products */}
|
||||
{!hideIconBar && <NavigationBar />}
|
||||
|
||||
{/* Product menu bar */}
|
||||
{productMenu && !showPausedState && (
|
||||
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
|
||||
)}
|
||||
|
||||
<main
|
||||
className="flex w-full flex-1 flex-col overflow-x-hidden"
|
||||
style={{ height: ongoingIncident ? 'calc(100vh - 44px)' : '100vh' }}
|
||||
>
|
||||
{!hideHeader && <LayoutHeader />}
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
<ProjectPausedState product={product} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
{/* Product menu bar */}
|
||||
{productMenu && !showPausedState && (
|
||||
<ProductMenuBar title={product}>{productMenu}</ProductMenuBar>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</ProjectContextProvider>
|
||||
|
||||
<main className="flex w-full flex-1 flex-col overflow-x-hidden">
|
||||
{!navLayoutV2 && !hideHeader && <LayoutHeader />}
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
<ProjectPausedState product={product} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</ProjectContextProvider>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useFlag, useSelectedOrganization, useSelectedProject, useStore, withAuth } from 'hooks'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
import { generateSettingsMenu } from './SettingsMenu.utils'
|
||||
|
||||
import ProductMenu from 'components/ui/ProductMenu'
|
||||
import ProjectLayout from '..'
|
||||
import { useParams } from 'common'
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
title?: string
|
||||
}
|
||||
|
||||
const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutProps>) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { ui, meta } = useStore()
|
||||
const project = useSelectedProject()
|
||||
const organization = useSelectedOrganization()
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
|
||||
// billing pages live under /billing/invoices and /billing/subscription, etc
|
||||
// so we need to pass the [5]th part of the url to the menu
|
||||
const page = router.pathname.includes('billing')
|
||||
? router.pathname.split('/')[5]
|
||||
: router.pathname.split('/')[4]
|
||||
|
||||
const isVaultEnabled = useFlag('vaultExtension')
|
||||
const menuRoutes = generateSettingsMenu(ref, project, isVaultEnabled, isOrgBilling)
|
||||
|
||||
useEffect(() => {
|
||||
if (ui.selectedProjectRef) {
|
||||
meta.extensions.load()
|
||||
}
|
||||
}, [ui.selectedProjectRef])
|
||||
|
||||
return (
|
||||
<ProjectLayout
|
||||
title={title || 'Settings'}
|
||||
product="Settings"
|
||||
productMenu={<ProductMenu page={page} menu={menuRoutes} />}
|
||||
>
|
||||
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
</ProjectLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(observer(SettingsLayout))
|
||||
File renamed without changes.
@@ -0,0 +1,28 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import SettingsMenuItem from './SettingsMenuItem'
|
||||
|
||||
const AccountSettingsMenu = () => {
|
||||
const router = useRouter()
|
||||
const accountSettings = [
|
||||
{ label: 'Preferences', pathname: `/account/me` },
|
||||
{ label: 'Access Tokens', pathname: `/account/tokens` },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Account Settings</p>
|
||||
{accountSettings.map((link) => (
|
||||
<SettingsMenuItem
|
||||
key={link.label}
|
||||
label={link.label}
|
||||
href={link.pathname}
|
||||
isActive={link.pathname === router.pathname}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountSettingsMenu
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import SettingsMenuItem from './SettingsMenuItem'
|
||||
|
||||
const OrganizationSettingsMenu = () => {
|
||||
const router = useRouter()
|
||||
const organization = useSelectedOrganization()
|
||||
const organizationSettings = [
|
||||
{ label: 'General', pathname: `/org/[slug]/general` },
|
||||
{ label: 'Members', pathname: `/org/[slug]/team` },
|
||||
{ label: 'Integrations', pathname: `/org/[slug]/integrations` },
|
||||
{ label: 'Billing', pathname: `/org/[slug]/billing` },
|
||||
{ label: 'Usage', pathname: `/org/[slug]/usage` },
|
||||
{ label: 'Invoices', pathname: `/org/[slug]/invoices` },
|
||||
{ label: 'OAuth Apps', pathname: `/org/[slug]/apps` },
|
||||
{ label: 'Audit Logs', pathname: `/org/[slug]/audit` },
|
||||
]
|
||||
|
||||
// const { data: allProjects } = useProjectsQuery()
|
||||
// const projects = allProjects?.filter((project) => project.organization_id === organization?.id)
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Organization</p>
|
||||
{organizationSettings.map((link) => (
|
||||
<SettingsMenuItem
|
||||
key={link.label}
|
||||
label={link.label}
|
||||
href={link.pathname.replace('[slug]', organization?.slug ?? '')}
|
||||
isActive={link.pathname === router.pathname}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* [Joshen] Hiding below until we figure out a better UX for consolidating settings on projects */}
|
||||
{/* <div className="space-y-2">
|
||||
<p className="text-sm font-medium">Projects</p>
|
||||
<Accordion_Shadcn_ type="single" collapsible className="w-full">
|
||||
{projects?.map((project) => (
|
||||
<ProjectSettingsMenuItem key={project.ref} project={project} />
|
||||
))}
|
||||
</Accordion_Shadcn_>
|
||||
</div>
|
||||
<div>
|
||||
<Link href={`/new/${organization?.slug}`}>
|
||||
<a>
|
||||
<Button type="default" size="small">
|
||||
Create new project
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrganizationSettingsMenu
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
AccordionContent_Shadcn_,
|
||||
AccordionItem_Shadcn_,
|
||||
AccordionTrigger_Shadcn_,
|
||||
IconChevronRight,
|
||||
} from 'ui'
|
||||
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { Project } from 'types'
|
||||
import SettingsMenuItem from './SettingsMenuItem'
|
||||
|
||||
interface ProjectSettingsMenuItemProps {
|
||||
project: Project
|
||||
}
|
||||
|
||||
const ProjectSettingsMenuItem = ({ project }: ProjectSettingsMenuItemProps) => {
|
||||
const router = useRouter()
|
||||
const organization = useSelectedOrganization()
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
|
||||
// [Joshen] Links need to be updated once we start implementing these
|
||||
const projectSettings = isOrgBilling
|
||||
? [
|
||||
{ label: 'General', pathname: `/project/[ref]/settings/general` },
|
||||
{ label: 'Infrastructure', pathname: `/project/[ref]/settings/infrastructure` },
|
||||
{ label: 'Add Ons', pathname: `/project/[ref]/settings/addons` },
|
||||
]
|
||||
: [
|
||||
{ label: 'General', pathname: `/project/[ref]/settings/general` },
|
||||
{ label: 'Subscription', pathname: `/project/[ref]/settings/billing/subscription` },
|
||||
{ label: 'Usage', pathname: `/project/[ref]/settings/billing/usage` },
|
||||
{ label: 'Invoices', pathname: `/project/[ref]/settings/billing/invoices` },
|
||||
]
|
||||
|
||||
return (
|
||||
<AccordionItem_Shadcn_ value={project.ref} className="!border-none">
|
||||
<AccordionTrigger_Shadcn_
|
||||
hideIcon
|
||||
className="py-1.5 hover:!no-underline [&[data-state=open]>div>svg]:!rotate-90 [&[data-state=open]>div>p]:text-scale-1200"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<IconChevronRight
|
||||
className="h-4 w-4 transition-transform duration-200"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<p className="text-scale-1100 text-sm font-normal transition duration-200">
|
||||
{project.name}
|
||||
</p>
|
||||
</div>
|
||||
</AccordionTrigger_Shadcn_>
|
||||
<AccordionContent_Shadcn_>
|
||||
<div className="space-y-2 pl-6 mt-1">
|
||||
{projectSettings.map((link) => {
|
||||
const href = link.pathname.replace('[ref]', project.ref)
|
||||
return (
|
||||
<SettingsMenuItem
|
||||
key={link.label}
|
||||
label={link.label}
|
||||
href={href}
|
||||
isActive={href === router.asPath}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</AccordionContent_Shadcn_>
|
||||
</AccordionItem_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectSettingsMenuItem
|
||||
@@ -1,51 +1,30 @@
|
||||
import { useFlag, useSelectedOrganization, useSelectedProject, useStore, withAuth } from 'hooks'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useRouter } from 'next/router'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
import { generateSettingsMenu } from './SettingsMenu.utils'
|
||||
|
||||
import { PropsWithChildren } from 'react'
|
||||
import AppLayout from '../AppLayout/AppLayout'
|
||||
import { useParams } from 'common'
|
||||
import ProductMenu from 'components/ui/ProductMenu'
|
||||
import ProjectLayout from '../'
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
title?: string
|
||||
}
|
||||
|
||||
const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutProps>) => {
|
||||
const { ref } = useParams()
|
||||
const { ui, meta } = useStore()
|
||||
const project = useSelectedProject()
|
||||
const organization = useSelectedOrganization()
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
import OrganizationSettingsMenu from './OrganizationSettingsMenu'
|
||||
import AccountSettingsMenu from './AccountSettingsMenu'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const SettingsLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const router = useRouter()
|
||||
// billing pages live under /billing/invoices and /billing/subscription, etc
|
||||
// so we need to pass the [5]th part of the url to the menu
|
||||
const page = router.pathname.includes('billing')
|
||||
? router.pathname.split('/')[5]
|
||||
: router.pathname.split('/')[4]
|
||||
|
||||
const isVaultEnabled = useFlag('vaultExtension')
|
||||
const menuRoutes = generateSettingsMenu(ref, project, isVaultEnabled, isOrgBilling)
|
||||
|
||||
useEffect(() => {
|
||||
if (ui.selectedProjectRef) {
|
||||
meta.extensions.load()
|
||||
}
|
||||
}, [ui.selectedProjectRef])
|
||||
const { ref, slug } = useParams()
|
||||
|
||||
return (
|
||||
<ProjectLayout
|
||||
title={title || 'Settings'}
|
||||
product="Settings"
|
||||
productMenu={<ProductMenu page={page} menu={menuRoutes} />}
|
||||
>
|
||||
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
</ProjectLayout>
|
||||
<AppLayout>
|
||||
<div className="flex h-full">
|
||||
{router.pathname !== '/projects' && (
|
||||
<div className="h-full overflow-y-auto min-w-[280px] border-r px-8 py-8">
|
||||
{slug === undefined && ref === undefined ? (
|
||||
<AccountSettingsMenu />
|
||||
) : (
|
||||
<OrganizationSettingsMenu />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full overflow-y-auto flex-grow">{children}</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default withAuth(observer(SettingsLayout))
|
||||
export default SettingsLayout
|
||||
@@ -0,0 +1,27 @@
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface SettingsMenuItemProps {
|
||||
href: string
|
||||
label: string
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
const SettingsMenuItem = ({ href, label, isActive }: SettingsMenuItemProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Link href={href}>
|
||||
<a
|
||||
className={clsx(
|
||||
'text-sm',
|
||||
isActive ? 'text-scale-1200' : 'text-scale-1100 hover:text-scale-1200 transition'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsMenuItem
|
||||
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'
|
||||
import { noop, partition } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dropdown,
|
||||
IconCheck,
|
||||
@@ -83,7 +84,6 @@ const TableEditorMenu = ({
|
||||
)
|
||||
|
||||
const schemas: PostgresSchema[] = meta.schemas.list()
|
||||
|
||||
const schema = schemas.find((schema) => schema.name === selectedSchema)
|
||||
const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
|
||||
|
||||
@@ -110,6 +110,13 @@ const TableEditorMenu = ({
|
||||
<IconLoader className="animate-spin" size={12} />
|
||||
<span className="text-xs text-scale-900">Loading schemas...</span>
|
||||
</div>
|
||||
) : meta.schemas.hasError ? (
|
||||
<Alert variant="warning" title="Failed to load schemas" className="!px-3 !py-3">
|
||||
<p className="mb-2">Error: {meta.schemas?.error?.message}</p>
|
||||
<Button type="default" size="tiny" onClick={() => meta.schemas.load()}>
|
||||
Reload schemas
|
||||
</Button>
|
||||
</Alert>
|
||||
) : (
|
||||
<Listbox
|
||||
size="tiny"
|
||||
|
||||
@@ -9,7 +9,7 @@ import TableEditorLayout from './TableEditorLayout/TableEditorLayout'
|
||||
import SQLEditorLayout from './SQLEditorLayout/SQLEditorLayout'
|
||||
import DatabaseLayout from './DatabaseLayout/DatabaseLayout'
|
||||
import DocsLayout from './DocsLayout/DocsLayout'
|
||||
import SettingsLayout from './SettingsLayout/SettingsLayout'
|
||||
import SettingsLayout from './ProjectSettingsLayout/SettingsLayout'
|
||||
import StorageLayout from './StorageLayout/StorageLayout'
|
||||
import AccountLayout from './AccountLayout/AccountLayout'
|
||||
import { AccountLayoutWithoutAuth } from './AccountLayout/AccountLayout'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
/**
|
||||
* Render keyMap
|
||||
*
|
||||
@@ -13,7 +11,7 @@ const customKeyChars: any = {
|
||||
enter: '↵',
|
||||
}
|
||||
|
||||
const KeyMap: FC<any> = ({ keyMap }) => {
|
||||
const KeyMap = ({ keyMap }: any) => {
|
||||
if (!keyMap) return null
|
||||
|
||||
const keys = keyMap.split('+')
|
||||
|
||||
@@ -39,7 +39,7 @@ const ProductEmptyState: FC<Props> = ({
|
||||
<div className="flex items-center space-x-2">
|
||||
{ctaButtonLabel && onClickCta && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button type="primary" onClick={onClickCta} disabled={disabled}>
|
||||
{ctaButtonLabel}
|
||||
</Button>
|
||||
|
||||
@@ -10,7 +10,6 @@ export async function getOrganizations(signal?: AbortSignal): Promise<Organizati
|
||||
if (data.error) throw data.error
|
||||
|
||||
const sorted = (data as Organization[]).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
return sorted
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,25 @@ import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useSelectedProject } from './useSelectedProject'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
|
||||
export function useSelectedOrganization({ enabled = true } = {}) {
|
||||
const { slug } = useParams()
|
||||
const { data } = useOrganizationsQuery({ enabled })
|
||||
const selectedProject = useSelectedProject()
|
||||
|
||||
const localStorageSlug = useMemo(() => {
|
||||
return typeof window !== 'undefined'
|
||||
? localStorage.getItem(LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION)
|
||||
: null
|
||||
}, [])
|
||||
|
||||
return useMemo(() => {
|
||||
return data?.find(
|
||||
(org) => org.slug === slug || (selectedProject && org.id === selectedProject.organization_id)
|
||||
)
|
||||
}, [data, selectedProject, slug])
|
||||
return data?.find((org) => {
|
||||
if (slug !== undefined) return org.slug === slug
|
||||
if (selectedProject !== undefined) return org.id === selectedProject.organization_id
|
||||
if (localStorageSlug !== undefined) return org.slug === localStorageSlug
|
||||
return undefined
|
||||
})
|
||||
}, [data, selectedProject, slug, localStorageSlug])
|
||||
}
|
||||
@@ -4,6 +4,5 @@ import { useProjectDetailQuery } from 'data/projects/project-detail-query'
|
||||
export function useSelectedProject() {
|
||||
const { ref } = useParams()
|
||||
const { data } = useProjectDetailQuery({ ref })
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -85,3 +85,7 @@ export const STRIPE_PUBLIC_KEY =
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY || 'pk_test_XVwg5IZH3I9Gti98hZw6KRzd00v5858heG'
|
||||
|
||||
export const USAGE_APPROACHING_THRESHOLD = 0.75
|
||||
|
||||
export const LOCAL_STORAGE_KEYS = {
|
||||
RECENTLY_VISITED_ORGANIZATION: 'supabase-organization',
|
||||
}
|
||||
@@ -62,6 +62,11 @@ dayjs.extend(relativeTime)
|
||||
|
||||
dart(Prism)
|
||||
|
||||
// [Joshen TODO] Once we settle on the new nav layout - we'll need a lot of clean up in terms of our layout components
|
||||
// a lot of them are unnecessary and introduce way too many cluttered CSS especially with the height styles that make
|
||||
// debugging way too difficult. Ideal scenario is we just have one AppLayout to control the height and scroll areas of
|
||||
// the dashboard, all other layout components should not be doing that
|
||||
|
||||
function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
const queryClient = useRootQueryClient()
|
||||
const [rootStore] = useState(() => new RootStore())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GeneralSettings } from 'components/interfaces/Organization'
|
||||
import { GeneralSettings as GeneralSettingsLegacy } from 'components/interfaces/Organization'
|
||||
import { OrganizationLayout } from 'components/layouts'
|
||||
import Loading from 'components/ui/Loading'
|
||||
import { usePermissionsQuery } from 'data/permissions/permissions-query'
|
||||
@@ -14,7 +14,7 @@ const OrgGeneralSettings: NextPageWithLayout = () => {
|
||||
{selectedOrganization === undefined && isLoadingPermissions ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<GeneralSettings />
|
||||
<GeneralSettingsLegacy />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import Link from 'next/link'
|
||||
import { Button, IconPlus } from 'ui'
|
||||
|
||||
import ProjectCard from 'components/interfaces/Home/ProjectList/ProjectCard'
|
||||
import ShimmeringCard from 'components/interfaces/Home/ProjectList/ShimmeringCard'
|
||||
import AppLayout from 'components/layouts/AppLayout/AppLayout'
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const ProjectsPage: NextPageWithLayout = () => {
|
||||
const {
|
||||
data: allProjects,
|
||||
error: projectsError,
|
||||
isLoading: isLoadingProjects,
|
||||
isError: isErrorProjects,
|
||||
isSuccess: isSuccessProjects,
|
||||
} = useProjectsQuery()
|
||||
|
||||
const organization = useSelectedOrganization()
|
||||
const projects = allProjects?.filter((project) => project.organization_id === organization?.id)
|
||||
|
||||
return (
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<div className="col-span-12 space-y-8">
|
||||
<Link href={`/new/${organization?.slug}`}>
|
||||
<a>
|
||||
<Button size="medium" type="default" iconRight={<IconPlus />}>
|
||||
New project
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-lg">Projects</h4>
|
||||
{isLoadingProjects && (
|
||||
<ul className="mx-auto grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<ShimmeringCard />
|
||||
<ShimmeringCard />
|
||||
</ul>
|
||||
)}
|
||||
{isErrorProjects && (
|
||||
<AlertError error={projectsError} subject="Failed to retrieve projects" />
|
||||
)}
|
||||
{isSuccessProjects && (
|
||||
<>
|
||||
{(projects?.length ?? 0) === 0 ? (
|
||||
<div className="col-span-4 space-y-4 rounded-lg border-2 border-dashed border-gray-300 p-6 text-center">
|
||||
<div className="space-y-1">
|
||||
<p>No projects</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Get started by creating a new project.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link href={`/new/${organization?.slug}`}>
|
||||
<a>
|
||||
<Button icon={<IconPlus />}>New Project</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="mx-auto grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{projects?.map((project) => (
|
||||
<ProjectCard key={project.ref} project={project} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
ProjectsPage.getLayout = (page) => <AppLayout>{page}</AppLayout>
|
||||
|
||||
export default ProjectsPage
|
||||
@@ -5,11 +5,15 @@ import AlertError from 'components/ui/AlertError'
|
||||
import Connecting from 'components/ui/Loading/Loading'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useAutoProjectsPrefetch } from 'data/projects/projects-query'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { useFlag } from 'hooks'
|
||||
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const ProjectsPage: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const {
|
||||
data: organizations,
|
||||
isLoading: isOrganizationLoading,
|
||||
@@ -19,24 +23,40 @@ const ProjectsPage: NextPageWithLayout = () => {
|
||||
useAutoProjectsPrefetch()
|
||||
|
||||
const { isLoading: isProfileLoading } = useProfile()
|
||||
|
||||
const isLoading = isOrganizationLoading || isProfileLoading
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const hasWindowLoaded = typeof window !== 'undefined'
|
||||
|
||||
useEffect(() => {
|
||||
if (navLayoutV2 && isSuccess && hasWindowLoaded) {
|
||||
const localStorageSlug = localStorage.getItem(
|
||||
LOCAL_STORAGE_KEYS.RECENTLY_VISITED_ORGANIZATION
|
||||
)
|
||||
const verifiedSlug = organizations.some((org) => org.slug === localStorageSlug)
|
||||
|
||||
if (organizations.length === 0) router.push('/new')
|
||||
else if (localStorageSlug && verifiedSlug) router.push(`/org/${localStorageSlug}`)
|
||||
else router.push(`/org/${organizations[0].slug}`)
|
||||
}
|
||||
}, [navLayoutV2, isSuccess, hasWindowLoaded])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center space-x-2">
|
||||
{navLayoutV2 && isLoading && (
|
||||
<div className={`flex items-center justify-center h-full`}>
|
||||
<Connecting />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<div className="py-4 px-5">
|
||||
<div
|
||||
className={`py-4 px-5 ${navLayoutV2 ? 'h-full flex items-center justify-center' : ''}`}
|
||||
>
|
||||
<AlertError subject="Failed to retrieve organizations" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSuccess && (
|
||||
{!navLayoutV2 && isSuccess && (
|
||||
<div className="py-4 px-5">
|
||||
{IS_PLATFORM && (
|
||||
<div className="my-2">
|
||||
|
||||
Reference in new issue
Block a user