Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import InformationBox from 'components/ui/InformationBox'
import { AlertCircle } from 'lucide-react'

interface NotOrganizationOwnerWarningProps {
slug?: string
}

// [Joshen] This can just use NoPermission component i think
const NotOrganizationOwnerWarning = () => {
const NotOrganizationOwnerWarning = ({ slug }: NotOrganizationOwnerWarningProps) => {
return (
<div className="mt-4">
<InformationBox
icon={<AlertCircle className="text-white" size="20" strokeWidth={1.5} />}
icon={<AlertCircle size="20" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="You do not have permission to create a project"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
Contact your organization owner or administrator to create a new project.
{slug ? (
<>
Contact the owner or administrator to create a new project in the{' '}
<code>{slug}</code> organization.
</>
) : (
<>Contact the owner or administrator to create a new project.</>
)}
</p>
</div>
}
Expand Down
48 changes: 48 additions & 0 deletions apps/studio/components/interfaces/Organization/OrgNotFound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import AlertError from 'components/ui/AlertError'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { Skeleton } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { OrganizationCard } from './OrganizationCard'

export const OrgNotFound = ({ slug }: { slug?: string }) => {
const {
data: organizations,
isSuccess: isOrganizationsSuccess,
isLoading: isOrganizationsLoading,
isError: isOrganizationsError,
error: organizationsError,
} = useOrganizationsQuery()

return (
<>
<Admonition type="danger">
The selected organization does not exist or you don't have permission to access it.{' '}
{slug ? (
<>
Contact the owner or administrator to create a new project in the <code>{slug}</code>{' '}
organization.
</>
) : (
<>Contact the owner or administrator to create a new project.</>
)}
</Admonition>

<h3 className="text-sm">Select an organization to create your new project from</h3>

<div className="grid gap-2 grid-cols-2">
{isOrganizationsLoading && (
<>
<Skeleton className="h-[62px] rounded-md" />
<Skeleton className="h-[62px] rounded-md" />
<Skeleton className="h-[62px] rounded-md" />
</>
)}
{isOrganizationsError && (
<AlertError error={organizationsError} subject="Failed to load organizations" />
)}
{isOrganizationsSuccess &&
organizations?.map((org) => <OrganizationCard key={org.slug} organization={org} />)}
</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Boxes } from 'lucide-react'
import Link from 'next/link'

import { ActionCard } from 'components/ui/ActionCard'
import { useProjectsQuery } from 'data/projects/projects-query'
import { Organization } from 'types'

export const OrganizationCard = ({ organization }: { organization: Organization }) => {
const { data: allProjects = [] } = useProjectsQuery()
const numProjects = allProjects.filter((x) => x.organization_slug === organization.slug).length

return (
<Link href={`/new/${organization.slug}`}>
<ActionCard
bgColor="bg border"
className="[&>div]:items-center"
icon={<Boxes size={18} strokeWidth={1} className="text-foreground" />}
title={organization.name}
description={`${organization.plan.name} Plan${numProjects > 0 ? `${' '}•${' '}${numProjects} project${numProjects > 1 ? 's' : ''}` : ''}`}
/>
</Link>
)
}
18 changes: 14 additions & 4 deletions apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,22 @@ export const OrganizationDropdown = () => {

return (
<>
<Link href={`/org/${slug}`} className="flex items-center gap-2 flex-shrink-0 text-sm">
<Link
href={slug ? `/org/${slug}` : '/organizations'}
className="flex items-center gap-2 flex-shrink-0 text-sm"
>
<Boxes size={14} strokeWidth={1.5} className="text-foreground-lighter" />
<span className="text-foreground max-w-32 lg:max-w-none truncate hidden md:block">
{orgName}
<span
className={cn(
'max-w-32 lg:max-w-none truncate hidden md:block',
!!selectedOrganization ? 'text-foreground' : 'text-foreground-lighter'
)}
>
{orgName ?? 'Select an organization'}
</span>
<Badge variant="default">{selectedOrganization?.plan.name}</Badge>
{!!selectedOrganization && (
<Badge variant="default">{selectedOrganization?.plan.name}</Badge>
)}
</Link>
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger_Shadcn_ asChild>
Expand Down
1 change: 1 addition & 0 deletions apps/studio/data/log-drains/update-log-drain-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export async function updateLogDrain(payload: LogDrainUpdateVariables) {
body: {
name: payload.name,
description: payload.description,
type: payload.type,
config: payload.config as any,
},
})
Expand Down
96 changes: 47 additions & 49 deletions apps/studio/pages/new/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
FreeProjectLimitWarning,
NotOrganizationOwnerWarning,
} from 'components/interfaces/Organization/NewProject'
import { OrgNotFound } from 'components/interfaces/Organization/OrgNotFound'
import { AdvancedConfiguration } from 'components/interfaces/ProjectCreation/AdvancedConfiguration'
import {
extractPostgresVersionDetails,
Expand Down Expand Up @@ -140,7 +141,6 @@ const Wizard: NextPageWithLayout = () => {
const { slug, projectName } = useParams()
const currentOrg = useSelectedOrganization()
const isFreePlan = currentOrg?.plan?.id === 'free'

const [lastVisitedOrganization] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
''
Expand Down Expand Up @@ -233,8 +233,11 @@ const Wizard: NextPageWithLayout = () => {
)

const isAdmin = useCheckPermissions(PermissionAction.CREATE, 'projects')

const isInvalidSlug = isOrganizationsSuccess && currentOrg === undefined
const orgNotFound = isOrganizationsSuccess && (organizations?.length ?? 0) > 0 && isInvalidSlug
const isEmptyOrganizations = (organizations?.length ?? 0) <= 0 && isOrganizationsSuccess

const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0

const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod'
Expand Down Expand Up @@ -408,14 +411,6 @@ const Wizard: NextPageWithLayout = () => {
if (projectName) form.setValue('projectName', projectName || '')
}, [slug])

useEffect(() => {
// Redirect to first org if the slug doesn't match an org slug
// this is mainly to capture the /new/new-project url, which is redirected from database.new
if (isInvalidSlug && isOrganizationsSuccess && (organizations?.length ?? 0) > 0) {
router.push(`/new/${organizations?.[0].slug}`)
}
}, [isInvalidSlug, isOrganizationsSuccess, organizations])

useEffect(() => {
if (form.getValues('dbRegion') === undefined && defaultRegion) {
form.setValue('dbRegion', defaultRegion)
Expand Down Expand Up @@ -537,7 +532,6 @@ const Wizard: NextPageWithLayout = () => {
Total Monthly Compute Costs
{/**
* API currently doesnt output replica information on the projects list endpoint. Until then, we cannot correctly calculate the costs including RRs.
*
* Will be adjusted in the future [kevin]
*/}
{organizationProjects.length > 0 && (
Expand Down Expand Up @@ -588,45 +582,49 @@ const Wizard: NextPageWithLayout = () => {
) : (
<div className="divide-y divide-border-muted">
<Panel.Content className={['space-y-4'].join(' ')}>
<FormField_Shadcn_
control={form.control}
name="organization"
render={({ field }) => (
<FormItemLayout label="Organization" layout="horizontal">
{(organizations?.length ?? 0) > 0 && (
<Select_Shadcn_
onValueChange={(slug) => {
field.onChange(slug)
router.push(`/new/${slug}`)
}}
value={field.value}
defaultValue={field.value}
>
<FormControl_Shadcn_>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select an organization" />
</SelectTrigger_Shadcn_>
</FormControl_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{organizations?.map((x) => (
<SelectItem_Shadcn_
key={x.id}
value={x.slug}
className="flex justify-between"
>
<span className="mr-2">{x.name}</span>
<Badge>{x.plan.name}</Badge>
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
)}
</FormItemLayout>
)}
/>
{!isAdmin && <NotOrganizationOwnerWarning />}
{isAdmin && !isInvalidSlug && (
<FormField_Shadcn_
control={form.control}
name="organization"
render={({ field }) => (
<FormItemLayout label="Organization" layout="horizontal">
{(organizations?.length ?? 0) > 0 && (
<Select_Shadcn_
onValueChange={(slug) => {
field.onChange(slug)
router.push(`/new/${slug}`)
}}
value={field.value}
defaultValue={field.value}
>
<FormControl_Shadcn_>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select an organization" />
</SelectTrigger_Shadcn_>
</FormControl_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{organizations?.map((x) => (
<SelectItem_Shadcn_
key={x.id}
value={x.slug}
className="flex justify-between"
>
<span className="mr-2">{x.name}</span>
<Badge>{x.plan.name}</Badge>
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
)}
</FormItemLayout>
)}
/>
)}

{!isAdmin && !orgNotFound && <NotOrganizationOwnerWarning slug={slug} />}
{orgNotFound && <OrgNotFound slug={slug} />}
</Panel.Content>

{canCreateProject && (
Expand Down
24 changes: 3 additions & 21 deletions apps/studio/pages/organizations.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { Boxes, Search } from 'lucide-react'
import { Search } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'

import { useParams } from 'common'
import { OrganizationCard } from 'components/interfaces/Organization/OrganizationCard'
import AppLayout from 'components/layouts/AppLayout/AppLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainerLegacy, ScaffoldTitle } from 'components/layouts/Scaffold'
import { ActionCard } from 'components/ui/ActionCard'
import AlertError from 'components/ui/AlertError'
import NoSearchResults from 'components/ui/NoSearchResults'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { withAuth } from 'hooks/misc/withAuth'
import { NextPageWithLayout } from 'types'
Expand All @@ -31,7 +30,6 @@ const OrganizationsPage: NextPageWithLayout = () => {
const { error: orgNotFoundError, org: orgSlug } = useParams()
const orgNotFound = orgNotFoundError === 'org_not_found'

const { data: projects = [] } = useProjectsQuery()
const { data: organizations = [], error, isLoading, isError, isSuccess } = useOrganizationsQuery()

const organizationCreationEnabled = useIsFeatureEnabled('organizations:create')
Expand Down Expand Up @@ -105,23 +103,7 @@ const OrganizationsPage: NextPageWithLayout = () => {
)}
{isError && <AlertError error={error} subject="Failed to load organizations" />}
{isSuccess &&
filteredOrganizations.map((organization) => {
const numProjects = projects.filter(
(x) => x.organization_slug === organization.slug
).length

return (
<ActionCard
bgColor="bg border"
className="[&>div]:items-center"
key={organization.id}
icon={<Boxes size={18} strokeWidth={1} className="text-foreground" />}
title={organization.name}
description={`${organization.plan.name} Plan${numProjects > 0 ? `${' '}•${' '}${numProjects} project${numProjects > 1 ? 's' : ''}` : ''}`}
onClick={() => router.push(`/org/${organization.slug}`)}
/>
)
})}
filteredOrganizations.map((org) => <OrganizationCard key={org.id} organization={org} />)}
</div>
</ScaffoldContainerLegacy>
)
Expand Down
9 changes: 5 additions & 4 deletions apps/www/components/Pricing/ComputePricingTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { Fragment, useMemo } from 'react'
import Link from 'next/link'
import { Fragment, useMemo } from 'react'

import { cn } from 'ui'
import pricingAddOn from '~/data/PricingAddOnTable.json'
import { IconPricingIncludedCheck, IconPricingMinus } from './PricingIcons'
import { cn } from 'ui'
import Link from 'next/link'

const ComputePricingTable = () => {
const columnNames = useMemo(
Expand Down Expand Up @@ -100,7 +101,7 @@ const ComputePricingTable = () => {
<IconPricingMinus plan="Free Plan" />
)
) : (
column.value + 'asd'
column.value
)}
</td>
</tr>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
},
"engines": {
"pnpm": ">=9",
"node": ">=20"
"node": ">=22"
},
"keywords": ["postgres", "firebase", "storage", "functions", "database", "auth"],
"packageManager": "[email protected]"
Expand Down
Loading
Loading