diff --git a/package-lock.json b/package-lock.json index 53406a85595..b0c58cc3eca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 88f5d9b0ef5..e53f54d332f 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -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' diff --git a/packages/ui/package.json b/packages/ui/package.json index 207a450235d..59f0f58500e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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", diff --git a/packages/ui/src/components/Dropdown/Dropdown.tsx b/packages/ui/src/components/Dropdown/Dropdown.tsx index 9e9d688f687..a1c1a3b25fe 100644 --- a/packages/ui/src/components/Dropdown/Dropdown.tsx +++ b/packages/ui/src/components/Dropdown/Dropdown.tsx @@ -103,7 +103,7 @@ export function Item({ icon, className, ...props }: ItemProps) { {...props} > {icon && icon} - {props.children} + {props.children} ) } diff --git a/packages/ui/src/components/shadcn/ui/accordion.tsx b/packages/ui/src/components/shadcn/ui/accordion.tsx index c5950299253..c5d5ec3dc15 100644 --- a/packages/ui/src/components/shadcn/ui/accordion.tsx +++ b/packages/ui/src/components/shadcn/ui/accordion.tsx @@ -16,8 +16,8 @@ AccordionItem.displayName = 'AccordionItem' const AccordionTrigger = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { hideIcon?: boolean } +>(({ className, children, hideIcon, ...props }, ref) => ( {children} - + {!hideIcon && } )) diff --git a/packages/ui/src/components/shadcn/ui/command.tsx b/packages/ui/src/components/shadcn/ui/command.tsx index 24f15e9e715..2f57d3bd0d7 100644 --- a/packages/ui/src/components/shadcn/ui/command.tsx +++ b/packages/ui/src/components/shadcn/ui/command.tsx @@ -70,8 +70,12 @@ CommandList.displayName = CommandPrimitive.List.displayName const CommandEmpty = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->((props, ref) => ( - +>(({ className, ...props }, ref) => ( + )) CommandEmpty.displayName = CommandPrimitive.Empty.displayName diff --git a/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx b/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx index 93eb4f2b669..e4141e48221 100644 --- a/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx +++ b/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx @@ -153,7 +153,7 @@ const DropdownMenuSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( )) diff --git a/studio/components/interfaces/Account/AccessTokenList.tsx b/studio/components/interfaces/Account/AccessTokenList.tsx index 5746aab2995..9b76f1c93b0 100644 --- a/studio/components/interfaces/Account/AccessTokenList.tsx +++ b/studio/components/interfaces/Account/AccessTokenList.tsx @@ -25,7 +25,7 @@ const AccessTokenList = observer(() => { return ( <> -
+
Token, diff --git a/studio/components/interfaces/App/AppBannerWrapper.tsx b/studio/components/interfaces/App/AppBannerWrapper.tsx index 3f3faa61251..581ca0c6669 100644 --- a/studio/components/interfaces/App/AppBannerWrapper.tsx +++ b/studio/components/interfaces/App/AppBannerWrapper.tsx @@ -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 ( diff --git a/studio/components/interfaces/App/RouteValidationWrapper.tsx b/studio/components/interfaces/App/RouteValidationWrapper.tsx index aacae694dc3..9fd0b0c8cbf 100644 --- a/studio/components/interfaces/App/RouteValidationWrapper.tsx +++ b/studio/components/interfaces/App/RouteValidationWrapper.tsx @@ -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} } diff --git a/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx b/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx index 20bf8488a3a..ad3689be4f9 100644 --- a/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx +++ b/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx @@ -24,7 +24,7 @@ const ProjectCard = ({ project, rewriteHref }: ProjectCardProps) => { const isRestoring = project.status === PROJECT_STATUS.RESTORING return ( -
  • +
  • { const queryClient = useQueryClient() diff --git a/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx b/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx index 5e81d11cadc..ddd6c3498f2 100644 --- a/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx +++ b/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx @@ -82,7 +82,7 @@ const InfrastructureInfo = () => { label="Current version" actions={[ isOnLatestVersion && ( - + Latest diff --git a/studio/components/layouts/AccountLayout/AccountLayout.tsx b/studio/components/layouts/AccountLayout/AccountLayout.tsx index bf3734d64af..7efdbaa5e11 100644 --- a/studio/components/layouts/AccountLayout/AccountLayout.tsx +++ b/studio/components/layouts/AccountLayout/AccountLayout.tsx @@ -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 { await signOut() - await router.push('/sign-in') } @@ -126,6 +127,10 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren{children} + } + return ( <> @@ -137,7 +142,12 @@ const AccountLayout = ({ children, title, breadcrumbs }: PropsWithChildren - + {children} diff --git a/studio/components/layouts/AccountLayout/WithSidebar.tsx b/studio/components/layouts/AccountLayout/WithSidebar.tsx index 4413ab6a3cb..cdf48e39a65 100644 --- a/studio/components/layouts/AccountLayout/WithSidebar.tsx +++ b/studio/components/layouts/AccountLayout/WithSidebar.tsx @@ -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 = ({ +const WithSidebar = ({ title, header, breadcrumbs = [], @@ -37,9 +37,10 @@ const WithSidebar: FC = ({ 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 = ({ )}
    - + {!navLayoutV2 && }
    {children}
    diff --git a/studio/components/layouts/AppLayout/AppHeader.tsx b/studio/components/layouts/AppLayout/AppHeader.tsx new file mode 100644 index 00000000000..5ea83c6e56a --- /dev/null +++ b/studio/components/layouts/AppLayout/AppHeader.tsx @@ -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 ( +
    +
    + + + Supabase + + + + {ref !== undefined && } +
    + +
    +
    + + + + +
    +
    + +
    +
    +
    + ) +} + +export default AppHeader diff --git a/studio/components/layouts/AppLayout/AppLayout.tsx b/studio/components/layouts/AppLayout/AppLayout.tsx new file mode 100644 index 00000000000..5abf9b3a826 --- /dev/null +++ b/studio/components/layouts/AppLayout/AppLayout.tsx @@ -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 ( +
    + {navLayoutV2 && } +
    + {children} +
    +
    + ) +} + +export default AppLayout diff --git a/studio/components/layouts/AppLayout/IncidentBanner.tsx b/studio/components/layouts/AppLayout/IncidentBanner.tsx new file mode 100644 index 00000000000..4004c1922cb --- /dev/null +++ b/studio/components/layouts/AppLayout/IncidentBanner.tsx @@ -0,0 +1,19 @@ +import Link from 'next/link' +import { IconExternalLink } from 'ui' + +const IncidentBanner = () => { + return ( + + +
    +

    + We are currently investigating a technical issue, follow status.supabase.com for updates +

    + +
    +
    + + ) +} + +export default IncidentBanner diff --git a/studio/components/layouts/AppLayout/OrganizationDropdown.tsx b/studio/components/layouts/AppLayout/OrganizationDropdown.tsx new file mode 100644 index 00000000000..035e4073307 --- /dev/null +++ b/studio/components/layouts/AppLayout/OrganizationDropdown.tsx @@ -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(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 + } + + return ( +
    + + +

    {orgName}

    + {isSuccess && {subscription?.plan.name}} +
    + + + + +
    + ) +} + +export default OrganizationDropdown diff --git a/studio/components/layouts/AppLayout/ProjectDropdown.tsx b/studio/components/layouts/AppLayout/ProjectDropdown.tsx new file mode 100644 index 00000000000..9a882441d23 --- /dev/null +++ b/studio/components/layouts/AppLayout/ProjectDropdown.tsx @@ -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 ( + + { + router.push(href) + setOpen(false) + }} + onClick={() => setOpen(false)} + > + + {project.name} + {project.ref === ref && } + + + + ) +} + +const ProjectDropdown = () => { + const router = useRouter() + const { ref } = useParams() + const projectDetails = useSelectedProject() + const selectedOrganization = useSelectedOrganization() + const projectNameRef = useRef(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 + } + + return IS_PLATFORM ? ( +
    + + +

    {selectedProject?.name}

    + {isSuccess && !isOrgBilling && {subscription?.plan.name}} +
    + + + + +
    + ) : ( + + ) +} + +export default ProjectDropdown diff --git a/studio/components/layouts/AppLayout/SettingsButton.tsx b/studio/components/layouts/AppLayout/SettingsButton.tsx new file mode 100644 index 00000000000..7576d779903 --- /dev/null +++ b/studio/components/layouts/AppLayout/SettingsButton.tsx @@ -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 ( + + + + + + + + + + + +
    + Settings +
    +
    +
    +
    + ) +} + +export default SettingsButton diff --git a/studio/components/layouts/AppLayout/UserSettingsDropdown.tsx b/studio/components/layouts/AppLayout/UserSettingsDropdown.tsx new file mode 100644 index 00000000000..b1e37ff0ef9 --- /dev/null +++ b/studio/components/layouts/AppLayout/UserSettingsDropdown.tsx @@ -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 ( + setOpen(!open)} modal={false}> + + {profile?.first_name ? profile?.first_name?.[0] : } + + + +
    +

    + {profile?.first_name} {profile?.last_name} +

    +

    {profile?.primary_email}

    +
    + + + + { + router.push('/account/me') + }} + onClick={() => setOpen(false)} + asChild + > + Preferences + + + + { + router.push('/account/tokens') + }} + onClick={() => setOpen(false)} + asChild + > + Access tokens + + + + { + setOpen(false) + setCommandMenuOpen(true) + }} + > + Command menu + ⌘K + + + Theme + { + const dark = x === 'dark' + toggleTheme(Boolean(dark)) + }} + > + Dark + Light + + + + { + onClickLogout() + setOpen(false) + }} + > + Log out + +
    +
    +
    + ) +} + +export default UserSettingsDropdown diff --git a/studio/components/layouts/OrganizationLayout.tsx b/studio/components/layouts/OrganizationLayout.tsx index 91ec8e0880a..691f99f25b5 100644 --- a/studio/components/layouts/OrganizationLayout.tsx +++ b/studio/components/layouts/OrganizationLayout.tsx @@ -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 {children} + } return ( { +interface FeedbackDropdownProps { + alt?: boolean +} + +const FeedbackDropdown = ({ alt = false }: FeedbackDropdownProps) => { const [isOpen, setIsOpen] = useState(false) const [feedback, setFeedback] = useState('') const [screenshot, setScreenshot] = useState() @@ -18,16 +29,29 @@ const FeedbackDropdown = () => { } return ( - { setIsOpen(e) if (!e) setScreenshot(undefined) }} - size="content" - side="bottom" - align="end" - overlay={ + > + + + + { screenshot={screenshot} setScreenshot={setScreenshot} /> - } - > - - + + ) } diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx index 0b9571b34ba..39851aa93b4 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx @@ -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 = ({ value={feedback} onChange={onFeedbackChange} /> - +
    - -
    {screenshot !== undefined ? (
    = ({ }} className="cursor-pointer rounded h-[26px] w-[30px] border border-scale-600 relative bg-cover bg-center bg-no-repeat" > -
    = ({ }} > -
    +
    ) : ( - + + + + + } - onClick={() => { + onSelect={() => { if (uploadButtonRef.current) (uploadButtonRef.current as any).click() }} > + Upload screenshot - , - + } - onClick={() => captureScreenshot()} + onSelect={() => captureScreenshot()} > + Capture screenshot - , - ]} - > - - + + + )} = () => { +const HelpPopover = ({ alt = false }: HelpPopoverProps) => { const router = useRouter() const projectRef = router.query.ref const supportUrl = `/support/new${projectRef ? `?ref=${projectRef}` : ''}` return ( - -
    -
    Need help with your project?
    -

    - For issues with your project hosted on supabase.com, or other inquiries about our - hosted services. -

    -
    - - - - - - - - - + + + + +
    +
    +
    +
    + {alt ? ( + + + +
    +

    Help

    +
    +
    +
    + ) : null} +
    + +
    +
    Need help with your project?
    +

    + For issues with your project hosted on supabase.com, or other inquiries about our hosted + services. +

    +
    + + + + + + + + + +
    +

    + Expected response time is based on your billing plan. Pro and Pay as You Go plans are + prioritized. +

    +
    + + + +
    +
    + +
    +
    +
    Reach out to the community
    +

    - 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.

    -
    - -
    + - -
    -
    -
    Reach out to the community
    - -

    - For other support, including questions on our client libraries, advice, or best - practices. -

    -
    -
    - } - > - - + + ) } diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx index 1ff8530f549..ebc0348ea3f 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/NotificationsPopover/index.tsx @@ -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 = () => { > -
    +
    + {hasNewNotifications && ( + <> + {alt ? null : ( +
    +
    +
    +
    + )} + + )} - {hasNewNotifications && ( -
    -
    -
    -
    - )}
    diff --git a/studio/components/layouts/ProjectLayout/LayoutHeader/ProjectDropdown.tsx b/studio/components/layouts/ProjectLayout/LayoutHeader/ProjectDropdown.tsx index 80152718fc4..232e27d29f5 100644 --- a/studio/components/layouts/ProjectLayout/LayoutHeader/ProjectDropdown.tsx +++ b/studio/components/layouts/ProjectLayout/LayoutHeader/ProjectDropdown.tsx @@ -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 + } + return IS_PLATFORM ? ( { } > - ) : ( - ) } diff --git a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index 50bdff1621b..4efb7bf31a4 100644 --- a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -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 = ({}) => { +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 = ({}) => { const { setIsOpen } = useCommandMenu() return (
      - - - Supabase - - + {!navLayoutV2 && ( + + + Supabase + + + )} = ({}) => { link: `/project/${projectRef}`, }} /> -
      +
      {toolRoutes.map((route) => ( = ({}) => { /> ))}
    -
      - {IS_PLATFORM && showCmdkHelper && ( - - - - - - - -
      + {IS_PLATFORM && showCmdkHelper && ( + + +
      -
      -
      -
      - )} - - {IS_PLATFORM && ( - <> - - }> - Account Preferences - - - - - )} - Theme - toggleTheme(e === 'dark')} - > - {/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */} - {/* System default */} - Dark - Light - - - } - > - - -
    +
    + +
    + + + + + +
    + {os === 'macos' ? ( + + ) : ( +

    CTRL

    + )} +

    K

    +
    +
    +
    + + )} + + {IS_PLATFORM && ( + <> + + } + > + Account Preferences + + + + + )} + Theme + toggleTheme(e === 'dark')} + > + {/* [Joshen] Removing system default for now, needs to be supported in useTheme from common packages */} + {/* System default */} + Dark + Light + + + } + > + + + + )}
    ) } diff --git a/studio/components/layouts/ProjectLayout/ProductMenuBar.tsx b/studio/components/layouts/ProjectLayout/ProductMenuBar.tsx index aac652b82f0..35f9456eb82 100644 --- a/studio/components/layouts/ProjectLayout/ProductMenuBar.tsx +++ b/studio/components/layouts/ProjectLayout/ProductMenuBar.tsx @@ -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 = ({ title, children }) => { - const ongoingIncident = useFlag('ongoingIncident') - const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh' - +const ProductMenuBar = ({ title, children }: PropsWithChildren) => { return (
    = ({ title, children }) => { >

    {title}

    -
    {children}
    +
    + {children} +
    ) } diff --git a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index 8d20baea54e..4b23582c1ef 100644 --- a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -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) => { 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 ( - - - - {title - ? `${title} | Supabase` - : selectedTable - ? `${selectedTable} | ${projectName} | ${organizationName} | Supabase` - : projectName - ? `${projectName} | ${organizationName} | Supabase` - : organizationName - ? `${organizationName} | Supabase` - : 'Supabase'} - - - -
    - {/* Left-most navigation side bar to access products */} - {!hideIconBar && } + + + + + {title + ? `${title} | Supabase` + : selectedTable + ? `${selectedTable} | ${projectName} | ${organizationName} | Supabase` + : projectName + ? `${projectName} | ${organizationName} | Supabase` + : organizationName + ? `${organizationName} | Supabase` + : 'Supabase'} + + + +
    + {/* Left-most navigation side bar to access products */} + {!hideIconBar && } - {/* Product menu bar */} - {!showPausedState && ( - - {productMenu} - - )} - -
    - {!hideHeader && } - {showPausedState ? ( -
    -
    - -
    -
    - ) : ( - {children} + {/* Product menu bar */} + {!showPausedState && ( + + {productMenu} + )} -
    -
    -
    + +
    + {!navLayoutV2 && !hideHeader && } + {showPausedState ? ( +
    +
    + +
    +
    + ) : ( + {children} + )} +
    +
    +
    + ) } @@ -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 ( - - - {title ? `${title} | Supabase` : 'Supabase'} - - - -
    - {/* Left-most navigation side bar to access products */} - {!hideIconBar && } + + + + {title ? `${title} | Supabase` : 'Supabase'} + + + +
    + {/* Left-most navigation side bar to access products */} + {!hideIconBar && } - {/* Product menu bar */} - {productMenu && !showPausedState && ( - {productMenu} - )} - -
    - {!hideHeader && } - {showPausedState ? ( -
    -
    - -
    -
    - ) : ( - children + {/* Product menu bar */} + {productMenu && !showPausedState && ( + {productMenu} )} -
    -
    -
    + +
    + {!navLayoutV2 && !hideHeader && } + {showPausedState ? ( +
    +
    + +
    +
    + ) : ( + children + )} +
    +
    +
    + ) } diff --git a/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx b/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx new file mode 100644 index 00000000000..2793fa28c80 --- /dev/null +++ b/studio/components/layouts/ProjectSettingsLayout/SettingsLayout.tsx @@ -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) => { + 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 ( + } + > +
    + {children} +
    +
    + ) +} + +export default withAuth(observer(SettingsLayout)) diff --git a/studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts b/studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts similarity index 100% rename from studio/components/layouts/SettingsLayout/SettingsMenu.utils.ts rename to studio/components/layouts/ProjectSettingsLayout/SettingsMenu.utils.ts diff --git a/studio/components/layouts/SettingsLayout/AccountSettingsMenu.tsx b/studio/components/layouts/SettingsLayout/AccountSettingsMenu.tsx new file mode 100644 index 00000000000..80c3b593dd3 --- /dev/null +++ b/studio/components/layouts/SettingsLayout/AccountSettingsMenu.tsx @@ -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 ( +
    +
    +

    Account Settings

    + {accountSettings.map((link) => ( + + ))} +
    +
    + ) +} + +export default AccountSettingsMenu diff --git a/studio/components/layouts/SettingsLayout/OrganizationSettingsMenu.tsx b/studio/components/layouts/SettingsLayout/OrganizationSettingsMenu.tsx new file mode 100644 index 00000000000..ee1ff6fe3d7 --- /dev/null +++ b/studio/components/layouts/SettingsLayout/OrganizationSettingsMenu.tsx @@ -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 ( +
    +
    +

    Organization

    + {organizationSettings.map((link) => ( + + ))} +
    + + {/* [Joshen] Hiding below until we figure out a better UX for consolidating settings on projects */} + {/*
    +

    Projects

    + + {projects?.map((project) => ( + + ))} + +
    + */} +
    + ) +} + +export default OrganizationSettingsMenu diff --git a/studio/components/layouts/SettingsLayout/ProjectSettingsMenuItem.tsx b/studio/components/layouts/SettingsLayout/ProjectSettingsMenuItem.tsx new file mode 100644 index 00000000000..2132e439e40 --- /dev/null +++ b/studio/components/layouts/SettingsLayout/ProjectSettingsMenuItem.tsx @@ -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 ( + + +
    + +

    + {project.name} +

    +
    +
    + +
    + {projectSettings.map((link) => { + const href = link.pathname.replace('[ref]', project.ref) + return ( + + ) + })} +
    +
    +
    + ) +} + +export default ProjectSettingsMenuItem diff --git a/studio/components/layouts/SettingsLayout/SettingsLayout.tsx b/studio/components/layouts/SettingsLayout/SettingsLayout.tsx index 61c49d15af0..81ec13f6e89 100644 --- a/studio/components/layouts/SettingsLayout/SettingsLayout.tsx +++ b/studio/components/layouts/SettingsLayout/SettingsLayout.tsx @@ -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) => { - 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 ( - } - > -
    - {children} -
    -
    + +
    + {router.pathname !== '/projects' && ( +
    + {slug === undefined && ref === undefined ? ( + + ) : ( + + )} +
    + )} +
    {children}
    +
    +
    ) } -export default withAuth(observer(SettingsLayout)) +export default SettingsLayout diff --git a/studio/components/layouts/SettingsLayout/SettingsMenuItem.tsx b/studio/components/layouts/SettingsLayout/SettingsMenuItem.tsx new file mode 100644 index 00000000000..e8bcf9b13a0 --- /dev/null +++ b/studio/components/layouts/SettingsLayout/SettingsMenuItem.tsx @@ -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 ( + + ) +} + +export default SettingsMenuItem diff --git a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 1e8f7e114e6..7e7190a73e9 100644 --- a/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -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 = ({ Loading schemas...
    + ) : meta.schemas.hasError ? ( + +

    Error: {meta.schemas?.error?.message}

    + +
    ) : ( = ({ keyMap }) => { +const KeyMap = ({ keyMap }: any) => { if (!keyMap) return null const keys = keyMap.split('+') diff --git a/studio/components/to-be-cleaned/ProductEmptyState.tsx b/studio/components/to-be-cleaned/ProductEmptyState.tsx index be23e0edc04..33ee6b1e1a8 100644 --- a/studio/components/to-be-cleaned/ProductEmptyState.tsx +++ b/studio/components/to-be-cleaned/ProductEmptyState.tsx @@ -39,7 +39,7 @@ const ProductEmptyState: FC = ({
    {ctaButtonLabel && onClickCta && ( - + diff --git a/studio/data/organizations/organizations-query.ts b/studio/data/organizations/organizations-query.ts index 98a2795b55b..dab843414ad 100644 --- a/studio/data/organizations/organizations-query.ts +++ b/studio/data/organizations/organizations-query.ts @@ -10,7 +10,6 @@ export async function getOrganizations(signal?: AbortSignal): Promise a.name.localeCompare(b.name)) - return sorted } diff --git a/studio/hooks/misc/useSelectedOrganization.ts b/studio/hooks/misc/useSelectedOrganization.ts index c2446380fc5..bc6be1b3710 100644 --- a/studio/hooks/misc/useSelectedOrganization.ts +++ b/studio/hooks/misc/useSelectedOrganization.ts @@ -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]) } diff --git a/studio/hooks/misc/useSelectedProject.ts b/studio/hooks/misc/useSelectedProject.ts index 824bb14a073..d5c1d82dfc2 100644 --- a/studio/hooks/misc/useSelectedProject.ts +++ b/studio/hooks/misc/useSelectedProject.ts @@ -4,6 +4,5 @@ import { useProjectDetailQuery } from 'data/projects/project-detail-query' export function useSelectedProject() { const { ref } = useParams() const { data } = useProjectDetailQuery({ ref }) - return data } diff --git a/studio/lib/constants/index.ts b/studio/lib/constants/index.ts index 24743c4904f..3e3b66f24b2 100644 --- a/studio/lib/constants/index.ts +++ b/studio/lib/constants/index.ts @@ -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', +} diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 6051ab68387..b0652d80767 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -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()) diff --git a/studio/pages/org/[slug]/general.tsx b/studio/pages/org/[slug]/general.tsx index 245a3e23f05..d186365fad5 100644 --- a/studio/pages/org/[slug]/general.tsx +++ b/studio/pages/org/[slug]/general.tsx @@ -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 ? ( ) : ( - + )} ) diff --git a/studio/pages/org/[slug]/index.tsx b/studio/pages/org/[slug]/index.tsx new file mode 100644 index 00000000000..b1e1245555c --- /dev/null +++ b/studio/pages/org/[slug]/index.tsx @@ -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 ( + + +
    + + + + + +
    +

    Projects

    + {isLoadingProjects && ( +
      + + +
    + )} + {isErrorProjects && ( + + )} + {isSuccessProjects && ( + <> + {(projects?.length ?? 0) === 0 ? ( +
    +
    +

    No projects

    +

    + Get started by creating a new project. +

    +
    + +
    + ) : ( +
      + {projects?.map((project) => ( + + ))} +
    + )} + + )} +
    +
    +
    +
    + ) +} + +ProjectsPage.getLayout = (page) => {page} + +export default ProjectsPage diff --git a/studio/pages/projects.tsx b/studio/pages/projects.tsx index 8fe0df5780d..5b26b3ccdb1 100644 --- a/studio/pages/projects.tsx +++ b/studio/pages/projects.tsx @@ -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 && ( -
    + {navLayoutV2 && isLoading && ( +
    )} {isError && ( -
    +
    )} - {isSuccess && ( + {!navLayoutV2 && isSuccess && (
    {IS_PLATFORM && (