From 470dd6e0f31b81750a1f2238c95e359b1b360feb Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Mon, 10 Feb 2025 13:45:32 +1000 Subject: [PATCH 01/28] fix layout issues (#33466) --- apps/studio/pages/project/[ref]/auth/advanced.tsx | 13 +++++++++---- apps/studio/pages/project/[ref]/auth/mfa.tsx | 13 +++++++++---- apps/studio/pages/project/[ref]/auth/protection.tsx | 8 ++++++-- apps/studio/pages/project/[ref]/auth/sessions.tsx | 13 +++++++++---- apps/studio/pages/project/[ref]/auth/smtp.tsx | 8 ++++++-- .../studio/pages/project/[ref]/auth/third-party.tsx | 8 ++++++-- 6 files changed, 45 insertions(+), 18 deletions(-) diff --git a/apps/studio/pages/project/[ref]/auth/advanced.tsx b/apps/studio/pages/project/[ref]/auth/advanced.tsx index a270d960910..5d39023c395 100644 --- a/apps/studio/pages/project/[ref]/auth/advanced.tsx +++ b/apps/studio/pages/project/[ref]/auth/advanced.tsx @@ -2,6 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { AdvancedAuthSettingsForm, SmtpForm, ThirdPartyAuthForm } from 'components/interfaces/Auth' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' +import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldDescription, @@ -13,7 +14,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' -const AdvancedAuthSettings: NextPageWithLayout = () => { +const PageLayout: NextPageWithLayout = () => { const isPermissionsLoaded = usePermissionsLoaded() // TODO: check if these permissions cover third party auth as well const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') @@ -40,7 +41,11 @@ const AdvancedAuthSettings: NextPageWithLayout = () => { ) } -AdvancedAuthSettings.getLayout = (page) => { - return {page} +PageLayout.getLayout = (page) => { + return ( + + {page} + + ) } -export default AdvancedAuthSettings +export default PageLayout diff --git a/apps/studio/pages/project/[ref]/auth/mfa.tsx b/apps/studio/pages/project/[ref]/auth/mfa.tsx index d56932a49a4..8bdcd91c193 100644 --- a/apps/studio/pages/project/[ref]/auth/mfa.tsx +++ b/apps/studio/pages/project/[ref]/auth/mfa.tsx @@ -2,6 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { MfaAuthSettingsForm } from 'components/interfaces/Auth' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' +import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldDescription, @@ -13,7 +14,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' -const AuthSettings: NextPageWithLayout = () => { +const PageLayout: NextPageWithLayout = () => { const isPermissionsLoaded = usePermissionsLoaded() // TODO: check if these permissions cover third party auth as well const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') @@ -43,7 +44,11 @@ const AuthSettings: NextPageWithLayout = () => { ) } -AuthSettings.getLayout = (page) => { - return {page} +PageLayout.getLayout = (page) => { + return ( + + {page} + + ) } -export default AuthSettings +export default PageLayout diff --git a/apps/studio/pages/project/[ref]/auth/protection.tsx b/apps/studio/pages/project/[ref]/auth/protection.tsx index e0651326bb3..6f7bbddef9d 100644 --- a/apps/studio/pages/project/[ref]/auth/protection.tsx +++ b/apps/studio/pages/project/[ref]/auth/protection.tsx @@ -6,6 +6,7 @@ import { ThirdPartyAuthForm, } from 'components/interfaces/Auth' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' +import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldHeader, ScaffoldContainer, ScaffoldTitle } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' @@ -35,7 +36,10 @@ const PageLayout: NextPageWithLayout = () => { } PageLayout.getLayout = (page) => { - return {page} + return ( + + {page} + + ) } - export default PageLayout diff --git a/apps/studio/pages/project/[ref]/auth/sessions.tsx b/apps/studio/pages/project/[ref]/auth/sessions.tsx index 2835de7767c..7dd24438315 100644 --- a/apps/studio/pages/project/[ref]/auth/sessions.tsx +++ b/apps/studio/pages/project/[ref]/auth/sessions.tsx @@ -2,13 +2,14 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { SessionsAuthSettingsForm, SmtpForm, ThirdPartyAuthForm } from 'components/interfaces/Auth' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' +import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldHeader, ScaffoldTitle } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' -const AuthSettings: NextPageWithLayout = () => { +const PageLayout: NextPageWithLayout = () => { const isPermissionsLoaded = usePermissionsLoaded() // TODO: check if these permissions cover third party auth as well const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') @@ -33,7 +34,11 @@ const AuthSettings: NextPageWithLayout = () => { ) } -AuthSettings.getLayout = (page) => { - return {page} +PageLayout.getLayout = (page) => { + return ( + + {page} + + ) } -export default AuthSettings +export default PageLayout diff --git a/apps/studio/pages/project/[ref]/auth/smtp.tsx b/apps/studio/pages/project/[ref]/auth/smtp.tsx index 9db26e4661e..51fe769af7c 100644 --- a/apps/studio/pages/project/[ref]/auth/smtp.tsx +++ b/apps/studio/pages/project/[ref]/auth/smtp.tsx @@ -16,6 +16,7 @@ import { } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import type { NextPageWithLayout } from 'types' +import DefaultLayout from 'components/layouts/DefaultLayout' const PageLayout: NextPageWithLayout = () => { const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') @@ -69,7 +70,10 @@ const PageLayout: NextPageWithLayout = () => { } PageLayout.getLayout = (page) => { - return {page} + return ( + + {page} + + ) } - export default PageLayout diff --git a/apps/studio/pages/project/[ref]/auth/third-party.tsx b/apps/studio/pages/project/[ref]/auth/third-party.tsx index c9782a0d636..f48b3eabf6a 100644 --- a/apps/studio/pages/project/[ref]/auth/third-party.tsx +++ b/apps/studio/pages/project/[ref]/auth/third-party.tsx @@ -16,6 +16,7 @@ import { } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' import type { NextPageWithLayout } from 'types' +import DefaultLayout from 'components/layouts/DefaultLayout' const PageLayout: NextPageWithLayout = () => { const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue') @@ -69,7 +70,10 @@ const PageLayout: NextPageWithLayout = () => { } PageLayout.getLayout = (page) => { - return {page} + return ( + + {page} + + ) } - export default PageLayout From 257824e31973df98b7d61376acb084cc7bc7783d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 10 Feb 2025 12:13:32 +0800 Subject: [PATCH 02/28] Chore/connect UI update for session pool mode (#33230) * Add admoniton in Connect UI for transaction pooling section if pooling mode is Session * Add callout in session pooler to use port 5432 * Update to use Panel.Notice * Add callout to use port 5432 for session in pooling configuration settings * Update warning when setting to txn mode from session * Minor refactors --------- Co-authored-by: Jonathan Summers-Muir --- .../interfaces/Connect/ConnectionPanel.tsx | 84 ++++++++++--- .../Connect/DatabaseConnectionString.tsx | 2 +- .../ConnectionPooling/ConnectionPooling.tsx | 42 ++++--- .../components/layouts/DefaultLayout.tsx | 7 +- apps/studio/components/ui/Panel.tsx | 114 ++++++++++-------- 5 files changed, 153 insertions(+), 96 deletions(-) diff --git a/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx b/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx index bce5602468d..807c47a7724 100644 --- a/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx +++ b/apps/studio/components/interfaces/Connect/ConnectionPanel.tsx @@ -1,6 +1,10 @@ import { ChevronRight, FileCode, X } from 'lucide-react' import Link from 'next/link' +import { useParams } from 'common' +import Panel from 'components/ui/Panel' +import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query' +import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { Button, cn, @@ -11,6 +15,7 @@ import { CollapsibleTrigger_Shadcn_, WarningIcon, } from 'ui' +import { Admonition } from 'ui-patterns' import { ConnectionParameters } from './ConnectionParameters' import { DirectConnectionIcon, TransactionIcon } from './PoolerIcons' @@ -105,6 +110,13 @@ export const ConnectionPanel = ({ fileTitle, onCopyCallback, }: ConnectionPanelProps) => { + const { ref: projectRef } = useParams() + const state = useDatabaseSelectorStateSnapshot() + + const { data: poolingInfo } = usePoolingConfigurationQuery({ projectRef }) + const poolingConfiguration = poolingInfo?.find((x) => x.identifier === state.selectedDatabaseId) + const isSessionMode = poolingConfiguration?.pool_mode === 'session' + return (
@@ -112,27 +124,59 @@ export const ConnectionPanel = ({

{description}

{fileTitle && } - - {notice && ( -
- {notice?.map((text: string) => ( -

- {text} -

- ))} -
+ {type === 'transaction' && isSessionMode ? ( + + + + ) : ( + <> + {/* [Joshen] Can probably remove this after Feb 28 */} + {type === 'session' && isSessionMode && ( + + )} + + {notice && ( +
+ {notice?.map((text: string) => ( +

+ {text} +

+ ))} +
+ )} + {parameters.length > 0 && } + )} - {parameters.length > 0 && }
diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx index 357ad0f78af..9e3817a5ff2 100644 --- a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx +++ b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx @@ -11,6 +11,7 @@ import { usePoolingConfigurationQuery } from 'data/database/pooling-configuratio import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { pluckObjectFields } from 'lib/helpers' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { @@ -36,7 +37,6 @@ import { import { CodeBlockFileHeader, ConnectionPanel } from './ConnectionPanel' import { getConnectionStrings } from './DatabaseSettings.utils' import examples, { Example } from './DirectConnectionExamples' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' const StepLabel = ({ number, diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx index 379691b2bab..e1a5a7fc8d4 100644 --- a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx +++ b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx @@ -36,6 +36,7 @@ import { Listbox, Separator, } from 'ui' +import { Admonition } from 'ui-patterns' import { SESSION_MODE_DESCRIPTION, TRANSACTION_MODE_DESCRIPTION } from '../Database.constants' import { POOLING_OPTIMIZATIONS } from './ConnectionPooling.constants' import { useAuthConfigQuery } from 'data/auth/auth-config-query' @@ -98,7 +99,7 @@ export const ConnectionPooling = () => { isLoading, isError, isSuccess, - } = usePoolingConfigurationQuery({ projectRef: projectRef }) + } = usePoolingConfigurationQuery({ projectRef }) const { data: authConfig } = useAuthConfigQuery({ projectRef }) @@ -297,29 +298,32 @@ export const ConnectionPooling = () => { <> {field.value === 'transaction' ? ( - - - Pool mode will be set to transaction permanently on port 6543 - - - This will take into effect once saved. You can use session mode by - pointing the pooler connection to use port 5432. - - + ) : ( - - - Set to transaction mode to use both pooling modes concurrently - - - Session mode can be used concurrently with transaction mode by + {/* [Joshen] Can probably remove this after Feb 28 */} + + )} diff --git a/apps/studio/components/layouts/DefaultLayout.tsx b/apps/studio/components/layouts/DefaultLayout.tsx index a397f020c8a..2e0bca40b33 100644 --- a/apps/studio/components/layouts/DefaultLayout.tsx +++ b/apps/studio/components/layouts/DefaultLayout.tsx @@ -1,11 +1,12 @@ +import { PropsWithChildren } from 'react' + +import { useParams } from 'common' import { AppBannerWrapper } from 'components/interfaces/App' import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext' -import { PropsWithChildren, ReactNode } from 'react' import { LayoutHeader } from './ProjectLayout/LayoutHeader' -import NavigationBar from './ProjectLayout/NavigationBar/NavigationBar' import MobileNavigationBar from './ProjectLayout/NavigationBar/MobileNavigationBar' +import NavigationBar from './ProjectLayout/NavigationBar/NavigationBar' import { ProjectContextProvider } from './ProjectLayout/ProjectContext' -import { useParams } from 'common' export interface DefaultLayoutProps { title?: string diff --git a/apps/studio/components/ui/Panel.tsx b/apps/studio/components/ui/Panel.tsx index fd3724b6372..dbc51b7dfa0 100644 --- a/apps/studio/components/ui/Panel.tsx +++ b/apps/studio/components/ui/Panel.tsx @@ -1,6 +1,7 @@ import { Megaphone } from 'lucide-react' import { forwardRef, PropsWithChildren, ReactNode } from 'react' -import { Badge, Button, Loading, cn } from 'ui' + +import { Badge, Button, cn, Loading } from 'ui' interface PanelProps { className?: string @@ -62,71 +63,78 @@ const PanelNotice = forwardRef< description?: string href?: string buttonText?: string + layout?: 'horizontal' | 'vertical' badgeLabel?: string } ->(({ className, title, description, href, buttonText, badgeLabel, ...props }, ref) => { - return ( -
+>( + ( + { + className, + title, + description, + href, + buttonText, + layout = 'horizontal', + badgeLabel, + ...props + }, + ref + ) => { + return (
+
-
-
- - - {badgeLabel ?? 'Upcoming change'} - - - {title} - - {/* {title} */} -
- {description && ( -
-
{description}
+ backgroundSize: '100% 100%, 15px 15px, 15px 15px', + backgroundPosition: '0 0, 0 0, 0 0', + }} + /> +
+
+ + + {badgeLabel ?? 'Upcoming change'} + + + {title} +
- )} -
- - {href && ( -
+ {description && ( +
+
{description}
+
+ )} +
+ {href && ( -
- )} -
- ) -}) + )} +
+ ) + } +) PanelNotice.displayName = 'PanelNotice' From 7de72b34f2586785918a00642de5845e94548bba Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Mon, 10 Feb 2025 13:14:44 +0800 Subject: [PATCH 03/28] fix: admonition styling fix, and ui patterns tailwind import fix (#33409) init --- apps/design-system/tailwind.config.js | 3 ++- apps/docs/tailwind.config.js | 3 ++- apps/studio/tailwind.config.js | 3 ++- apps/www/tailwind.config.js | 3 ++- packages/ui-patterns/admonition.tsx | 10 +++++----- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/design-system/tailwind.config.js b/apps/design-system/tailwind.config.js index 5a512287d82..5be9a0d5a1d 100644 --- a/apps/design-system/tailwind.config.js +++ b/apps/design-system/tailwind.config.js @@ -8,7 +8,8 @@ module.exports = config({ // purge styles from grid library // './../../packages/ui/src/**/*.{tsx,ts,js}', - './../../packages/ui-patterns/!(node_modules)/**/*.{tsx,ts,js}', + './../../packages/ui-patterns/**/*.{tsx,ts,js}', + '!./../../packages/ui-patterns/node_modules/**/*', // ignore node_modules rule ], theme: { extend: { diff --git a/apps/docs/tailwind.config.js b/apps/docs/tailwind.config.js index eb86da3cc96..176e4401cee 100644 --- a/apps/docs/tailwind.config.js +++ b/apps/docs/tailwind.config.js @@ -10,7 +10,8 @@ module.exports = config({ './layouts/**/*.tsx', './pages/**/*.{tsx,mdx}', './../../packages/ui/src/**/*.{tsx,ts,js}', - './../../packages/ui-patterns/!(node_modules)/**/*.{tsx,ts,js}', + './../../packages/ui-patterns/**/*.{tsx,ts,js}', + '!./../../packages/ui-patterns/node_modules/**/*', // ignore node_modules rule ], plugins: [ function ({ addUtilities, addVariant }) { diff --git a/apps/studio/tailwind.config.js b/apps/studio/tailwind.config.js index e75ba3b554e..ee13f7833a8 100644 --- a/apps/studio/tailwind.config.js +++ b/apps/studio/tailwind.config.js @@ -6,7 +6,8 @@ module.exports = config({ './components/**/*.{js,ts,jsx,tsx}', // purge styles from grid library './../../packages/ui/src/**/*.{tsx,ts,js}', - './../../packages/ui-patterns/!(node_modules)/**/*.{tsx,ts,js}', + './../../packages/ui-patterns/**/*.{tsx,ts,js}', + '!./../../packages/ui-patterns/node_modules/**/*', // ignore node_modules rule ], theme: { extend: { diff --git a/apps/www/tailwind.config.js b/apps/www/tailwind.config.js index 2f59a96c6c4..61fd861a21b 100644 --- a/apps/www/tailwind.config.js +++ b/apps/www/tailwind.config.js @@ -9,7 +9,8 @@ module.exports = config({ './lib/mdx/mdxComponents.tsx', './pages/**/*.{tsx,mdx}', './../../packages/ui/src/**/*.{tsx,ts,js}', - './../../packages/ui-patterns/!(node_modules)/**/*.{tsx,ts,js}', + './../../packages/ui-patterns/**/*.{tsx,ts,js}', + '!./../../packages/ui-patterns/node_modules/**/*', // ignore node_modules rule ], theme: { extend: { diff --git a/packages/ui-patterns/admonition.tsx b/packages/ui-patterns/admonition.tsx index 40a72225c05..89c2fc3c77e 100644 --- a/packages/ui-patterns/admonition.tsx +++ b/packages/ui-patterns/admonition.tsx @@ -69,9 +69,9 @@ const WarningIcon = () => ( const admonitionSVG = cva('', { variants: { type: { - default: `[&>svg]:bg-foreground-muted [&>svg]:text-background-muted`, - warning: `[&>svg]:bg-warning [&>svg]:text-warning-100`, - destructive: `[&>svg]:bg-destructive [&>svg]:text-white`, + default: `[&>svg]:bg-foreground-muted`, + warning: ``, + destructive: ``, }, }, }) @@ -80,7 +80,7 @@ const admonitionBase = cva('', { variants: { type: { default: `bg-surface-200/25 border border-default`, - warning: `bg-alternative border border-default ![&_p]:text-background-muted`, + warning: `bg-alternative border border-default`, destructive: `bg-alternative border border-default`, }, }, @@ -96,13 +96,13 @@ export const Admonition = forwardRef< ) => { const typeMapped = variant ? admonitionToAlertMapping[variant] : admonitionToAlertMapping[type] + return ( Date: Mon, 10 Feb 2025 01:43:21 -0500 Subject: [PATCH 04/28] fix: fly deprecation notice type (#33429) Deprecation notices should have type deprecated. --- apps/docs/content/guides/platform/fly-postgres.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/platform/fly-postgres.mdx b/apps/docs/content/guides/platform/fly-postgres.mdx index cc9690c3268..47f96ab8ad5 100644 --- a/apps/docs/content/guides/platform/fly-postgres.mdx +++ b/apps/docs/content/guides/platform/fly-postgres.mdx @@ -6,7 +6,7 @@ description: 'Deploy Supabase projects on the Fly.io platform' Fly Postgres databases are deployed on the Fly.io edge network. Fly Postgres is supported in [every region](https://fly.io/docs/reference/regions/) where Fly.io operates. - + Fly Postgres is being deprecated on March 14, 2025. Head over to the [announcement](https://github.com/orgs/supabase/discussions/33413) for more information. From 15841c6bf37a8dce7bf76c56a67c11fc7fae6b98 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Mon, 10 Feb 2025 16:45:36 +1000 Subject: [PATCH 05/28] track source when viewing plans (#33405) * track source when viewing plans * remove log * rename event * Update apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx Co-authored-by: Long Hoang <1732217+loong@users.noreply.github.com> * Update apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx Co-authored-by: Long Hoang <1732217+loong@users.noreply.github.com> * Update apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx Co-authored-by: Long Hoang <1732217+loong@users.noreply.github.com> * Update apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx Co-authored-by: Long Hoang <1732217+loong@users.noreply.github.com> * added docs for tracking * prettier * chore: rename origins to give more hints on where these are coming from --------- Co-authored-by: Long Hoang <1732217+loong@users.noreply.github.com> --- .../DataWarehouse/CollectionForm.tsx | 4 +++- .../Database/Backups/BackupsList.tsx | 1 + .../DiskManagement/DiskManagementForm.tsx | 6 +++++- .../DiskManagement/DiskManagementPanelForm.tsx | 2 +- .../Organization/AuditLogs/AuditLogs.tsx | 2 +- .../BillingBreakdown/BillingMetric.tsx | 6 +++++- .../BillingSettings/Restriction.tsx | 18 +++++++++++++++--- .../Subscription/PaymentMethodSelection.tsx | 2 +- .../Subscription/PlanUpdateSidePanel.tsx | 15 +++++++++++---- .../interfaces/Organization/Documents/SOC2.tsx | 2 +- .../Documents/SecurityQuestionnaire.tsx | 4 +++- .../Organization/NewOrg/NewOrgForm.tsx | 5 ++++- .../NewProject/FreeProjectLimitWarning.tsx | 6 +++++- .../TeamSettings/InviteMemberButton.tsx | 4 +++- .../Organization/Usage/Usage.utils.ts | 4 ++-- .../Usage/UsageSection/AttributeUsage.tsx | 2 +- .../Addons/ComputeInstanceSidePanel.tsx | 4 +++- .../Settings/Addons/CustomDomainSidePanel.tsx | 4 +++- .../Settings/Addons/IPv4SidePanel.tsx | 4 +++- .../Settings/Addons/PITRSidePanel.tsx | 4 +++- .../Database/DatabaseReadOnlyAlert.tsx | 8 ++++++-- .../CustomDomainConfig/CustomDomainConfig.tsx | 1 + .../DeployNewReplicaPanel.tsx | 4 ++-- .../interfaces/Settings/Logs/UpgradePrompt.tsx | 6 +++++- .../Support/PlanExpectationInfoBox.tsx | 6 +++++- .../BranchingPlanNotice.tsx | 2 +- .../PausedState/ProjectPausedState.tsx | 4 +++- .../StorageSettings/StorageSettings.tsx | 1 + apps/studio/components/ui/UpgradeToPro.tsx | 6 ++++-- .../project/[ref]/database/backups/pitr.tsx | 1 + .../backups/restore-to-new-project.tsx | 1 + packages/common/telemetry-constants.ts | 8 +++++++- 32 files changed, 111 insertions(+), 36 deletions(-) diff --git a/apps/studio/components/interfaces/DataWarehouse/CollectionForm.tsx b/apps/studio/components/interfaces/DataWarehouse/CollectionForm.tsx index 98543751db2..5bb7e906d59 100644 --- a/apps/studio/components/interfaces/DataWarehouse/CollectionForm.tsx +++ b/apps/studio/components/interfaces/DataWarehouse/CollectionForm.tsx @@ -127,7 +127,9 @@ export const CollectionForm = ({ className="mt-2" type="primary" onClick={() => - router.push(`/org/${currentOrg?.slug}/billing?panel=subscriptionPlan`) + router.push( + `/org/${currentOrg?.slug}/billing?panel=subscriptionPlan&source=dataWarehouseIncreaseRetention` + ) } > Upgrade plan diff --git a/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx b/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx index 48a962d8840..ac58b65c35b 100644 --- a/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -62,6 +62,7 @@ const BackupsList = () => { icon={} primaryText="Free Plan does not include project backups." secondaryText="Upgrade to the Pro Plan for up to 7 days of scheduled backups." + source="backups" /> ) } diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 84a224ed922..784bc7e930a 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -314,7 +314,11 @@ export function DiskManagementForm() { title="Compute and Disk configuration is not available on the Free Plan" actions={ } description="You will need to upgrade to at least the Pro Plan to configure compute and disk" diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx index 7f42ba51bce..a0c7d66194c 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementPanelForm.tsx @@ -316,7 +316,7 @@ export function DiskManagementPanelForm() { Upgrade plan diff --git a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 15b82f94d7d..5abde526b91 100644 --- a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -166,7 +166,7 @@ const AuditLogs = () => {
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx index e32f8ed77fd..110578487aa 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx @@ -143,7 +143,11 @@ const BillingMetric = ({ ) : (
)} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx index 4d5dd46d636..fce069a56d4 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Restriction.tsx @@ -97,7 +97,11 @@ export const Restriction = () => {

) : ( diff --git a/apps/studio/components/interfaces/Organization/Documents/SecurityQuestionnaire.tsx b/apps/studio/components/interfaces/Organization/Documents/SecurityQuestionnaire.tsx index 9aa74921ba5..28b6d1704c5 100644 --- a/apps/studio/components/interfaces/Organization/Documents/SecurityQuestionnaire.tsx +++ b/apps/studio/components/interfaces/Organization/Documents/SecurityQuestionnaire.tsx @@ -74,7 +74,9 @@ const SecurityQuestionnaire = () => { {isSuccess && (
{currentPlan?.id === 'free' || currentPlan?.id === 'pro' ? ( - + ) : ( diff --git a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index 6cc226b233d..c306c9f87d9 100644 --- a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -305,7 +305,10 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {

The plan applies only to this new organization. To upgrade an existing organization,{' '} - + click here . diff --git a/apps/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx b/apps/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx index 0c4923d91fe..6411fbe4571 100644 --- a/apps/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx +++ b/apps/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx @@ -38,7 +38,11 @@ const FreeProjectLimitWarning = ({

diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx index 56b641157f8..f7f0a9d7e51 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx @@ -408,7 +408,9 @@ export const InviteMemberButton = () => { {isSuccessSubscription && (currentPlan?.id === 'free' || currentPlan?.id === 'pro') && ( diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.utils.ts b/apps/studio/components/interfaces/Organization/Usage/Usage.utils.ts index 62dcce33ae1..73feec66168 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.utils.ts +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.utils.ts @@ -13,14 +13,14 @@ export const generateUsageData = (attribute: string, days: number): DataPoint[] }) } -export const getUpgradeUrl = (slug: string, subscription?: OrgSubscription) => { +export const getUpgradeUrl = (slug: string, subscription?: OrgSubscription, source?: string) => { if (!subscription) { return `/org/${slug}/billing` } return subscription?.plan?.id === 'pro' && subscription?.usage_billing_enabled === false ? `/org/${slug}/billing#cost-control` - : `/org/${slug}/billing?panel=subscriptionPlan` + : `/org/${slug}/billing?panel=subscriptionPlan&source=usage${source}` } const compactNumberFormatter = new Intl.NumberFormat('en-US', { diff --git a/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx index 517b5ae00f4..d48d114fed4 100644 --- a/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/UsageSection/AttributeUsage.tsx @@ -53,7 +53,7 @@ const AttributeUsage = ({ isSuccess, currentBillingCycleSelected, }: AttributeUsageProps) => { - const upgradeUrl = getUpgradeUrl(slug ?? '', subscription) + const upgradeUrl = getUpgradeUrl(slug ?? '', subscription, attribute.key) const usageRatio = (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0) const usageExcess = (usageMeta?.usage ?? 0) - (usageMeta?.pricing_free_units ?? 0) const usageBasedBilling = subscription?.usage_billing_enabled diff --git a/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx index a7cd645cfce..01b7109c670 100644 --- a/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx @@ -282,7 +282,9 @@ const ComputeInstanceSidePanel = () => { title="Changing your compute size is only available on the Pro Plan" actions={ diff --git a/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx index efc429764c8..c3c66febde3 100644 --- a/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/CustomDomainSidePanel.tsx @@ -229,7 +229,9 @@ const CustomDomainSidePanel = () => { title="Custom domains are unavailable on the Free Plan" actions={ diff --git a/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx index 5958fdb3060..eaa3c0e1c42 100644 --- a/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx @@ -222,7 +222,9 @@ const IPv4SidePanel = () => {

Upgrade your plan to enable a IPv4 address for your project

diff --git a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx index 62b21a77f8f..474d6d8dbef 100644 --- a/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/PITRSidePanel.tsx @@ -279,7 +279,9 @@ const PITRSidePanel = () => { title="Changing your Point-In-Time-Recovery is only available on the Pro Plan" actions={ diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseReadOnlyAlert.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseReadOnlyAlert.tsx index 3ecae543018..930aed2d448 100644 --- a/apps/studio/components/interfaces/Settings/Database/DatabaseReadOnlyAlert.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseReadOnlyAlert.tsx @@ -39,14 +39,18 @@ export const DatabaseReadOnlyAlert = () => { {subscription?.plan.id === 'free' ? (
  • - + Upgrade to the Pro Plan {' '} to increase your database size limit to 8GB.
  • ) : subscription?.plan.id === 'pro' && subscription?.usage_billing_enabled ? (
  • - + Disable your Spend Cap {' '} to allow your project to auto-scale and expand beyond the 8GB database size limit diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx index fcfe9c4f4f3..9491f6c6d5d 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx @@ -90,6 +90,7 @@ const CustomDomainConfig = () => { : 'To configure a custom domain for your project, please enable the add-on' } addon="customDomain" + source="customDomain" disabled={customDomainsDisabledDueToQuota} /> ) : ( diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx index 83f3adab5ad..1d52a9242d0 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DeployNewReplicaPanel.tsx @@ -301,7 +301,7 @@ const DeployNewReplicaPanel = ({ = ({ show, setShowUpgradePrompt }) => { Close diff --git a/apps/studio/components/interfaces/Support/PlanExpectationInfoBox.tsx b/apps/studio/components/interfaces/Support/PlanExpectationInfoBox.tsx index 994c70f5c16..cf4a34b5528 100644 --- a/apps/studio/components/interfaces/Support/PlanExpectationInfoBox.tsx +++ b/apps/studio/components/interfaces/Support/PlanExpectationInfoBox.tsx @@ -56,7 +56,11 @@ export const PlanExpectationInfoBox = ({
    diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx index cabedb71ae5..d0d37954f61 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx @@ -268,6 +268,7 @@ const StorageSettings = () => { icon={} primaryText="Free Plan has a fixed upload file size limit of 50 MB." secondaryText="Upgrade to the Pro Plan for a configurable upload file size limit of up to 50 GB." + source="storageSizeLimit" />
    )} diff --git a/apps/studio/components/ui/UpgradeToPro.tsx b/apps/studio/components/ui/UpgradeToPro.tsx index c3b9e00301e..b1979f74866 100644 --- a/apps/studio/components/ui/UpgradeToPro.tsx +++ b/apps/studio/components/ui/UpgradeToPro.tsx @@ -16,6 +16,7 @@ interface UpgradeToProProps { secondaryText: string addon?: 'pitr' | 'customDomain' | 'computeInstance' buttonText?: string + source?: string disabled?: boolean } @@ -25,6 +26,7 @@ const UpgradeToPro = ({ secondaryText, addon, buttonText, + source = 'upgrade', disabled = false, }: UpgradeToProProps) => { const project = useSelectedProject() @@ -80,8 +82,8 @@ const UpgradeToPro = ({ {buttonText || (plan === 'free' ? 'Upgrade to Pro' : 'Enable add on')} diff --git a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx index 04cfdb5d989..2a38ea3c293 100644 --- a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -89,6 +89,7 @@ const PITR = () => { {!isEnabled ? ( { return ( diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 9d416c71366..31c38e82a07 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -1069,7 +1069,13 @@ export interface StudioPricingPlanCtaClickedEvent { */ export interface StudioPricingSidePanelOpenedEvent { action: TelemetryActions.STUDIO_PRICING_SIDE_PANEL_OPENED - properties: { currentPlan?: string } + properties: { + currentPlan?: string + /** + * Tracks how user landed on the Pricing side panel, e.g. diskManagementPanelDiskSize, backupsRestoreToNewProject + */ + origin?: string + } groups: { organization: string } } From 0a569b8aa72f6e2abeaa10bfc0251243103bfdf7 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 10 Feb 2025 15:09:27 +0800 Subject: [PATCH 06/28] Add product empty state to schema visualizer (#33471) --- .../Database/Schemas/SchemaGraph.tsx | 89 +++++++++++-------- .../to-be-cleaned/ProductEmptyState.tsx | 11 ++- 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx index 706c526cfd5..7bd792f6b5e 100644 --- a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx +++ b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx @@ -7,6 +7,7 @@ import 'reactflow/dist/style.css' import { useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import SchemaSelector from 'components/ui/SchemaSelector' @@ -147,42 +148,58 @@ export const SchemaGraph = () => {
  • )} {isSuccessTables && ( -
    - saveNodePositions()} - > - - - - -
    + <> + {tables.length === 0 ? ( +
    + +

    + There are no tables found in the schema "{selectedSchema}" +

    +
    +
    + ) : ( +
    + saveNodePositions()} + > + + + + +
    + )} + )} ) diff --git a/apps/studio/components/to-be-cleaned/ProductEmptyState.tsx b/apps/studio/components/to-be-cleaned/ProductEmptyState.tsx index 7255dde4c4c..2b7d675a58a 100644 --- a/apps/studio/components/to-be-cleaned/ProductEmptyState.tsx +++ b/apps/studio/components/to-be-cleaned/ProductEmptyState.tsx @@ -2,6 +2,7 @@ import { ExternalLink } from 'lucide-react' import { PropsWithChildren } from 'react' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import Link from 'next/link' import { Button } from 'ui' interface ProductEmptyStateProps { @@ -14,6 +15,7 @@ interface ProductEmptyStateProps { loading?: boolean disabled?: boolean disabledMessage?: string + ctaUrl?: string } const ProductEmptyState = ({ @@ -27,6 +29,7 @@ const ProductEmptyState = ({ loading = false, disabled = false, disabledMessage = '', + ctaUrl, }: PropsWithChildren) => { const hasAction = (ctaButtonLabel && onClickCta) || (infoButtonUrl && infoButtonLabel) @@ -40,7 +43,11 @@ const ProductEmptyState = ({
    {children}
    {hasAction && (
    - {ctaButtonLabel && onClickCta && ( + {ctaButtonLabel && !!ctaUrl ? ( + + ) : ctaButtonLabel && !!onClickCta ? ( {ctaButtonLabel} - )} + ) : null} {infoButtonUrl && infoButtonLabel ? ( +} +``` + +## SidebarRail + +The `SidebarRail` component is used to render a rail within a `Sidebar`. This rail can be used to toggle the sidebar. + +```tsx showLineNumbers + + + + + + + + +``` + +## Data Fetching + +### React Server Components + +Here's an example of a `SidebarMenu` component rendering a list of projects using React Server Components. + +
    + +
    + A sidebar menu using React Server Components. +
    +
    + +```tsx showLineNumbers {6} title="Skeleton to show loading state." +function NavProjectsSkeleton() { + return ( + + {Array.from({ length: 5 }).map((_, index) => ( + + + + ))} + + ) +} +``` + +```tsx showLineNumbers {2} title="Server component fetching data." +async function NavProjects() { + const projects = await fetchProjects() + + return ( + + {projects.map((project) => ( + + + + + {project.name} + + + + ))} + + ) +} +``` + +```tsx showLineNumbers {8-10} title="Usage with React Suspense." +function AppSidebar() { + return ( + + + + Projects + + }> + + + + + + + ) +} +``` + +### SWR and React Query + +You can use the same approach with [SWR](https://swr.vercel.app/) or [react-query](https://tanstack.com/query/latest/docs/framework/react/overview). + +```tsx showLineNumbers title="SWR" +function NavProjects() { + const { data, isLoading } = useSWR("/api/projects", fetcher) + + if (isLoading) { + return ( + + {Array.from({ length: 5 }).map((_, index) => ( + + + + ))} + + ) + } + + if (!data) { + return ... + } + + return ( + + {data.map((project) => ( + + + + + {project.name} + + + + ))} + + ) +} +``` + +```tsx showLineNumbers title="React Query" +function NavProjects() { + const { data, isLoading } = useQuery() + + if (isLoading) { + return ( + + {Array.from({ length: 5 }).map((_, index) => ( + + + + ))} + + ) + } + + if (!data) { + return ... + } + + return ( + + {data.map((project) => ( + + + + + {project.name} + + + + ))} + + ) +} +``` + +## Controlled Sidebar + +Use the `open` and `onOpenChange` props to control the sidebar. + +
    + +
    A controlled sidebar.
    +
    + +```tsx showLineNumbers +export function AppSidebar() { + const [open, setOpen] = React.useState(false) + + return ( + + + + ) +} +``` + +## Theming + +We use the following CSS variables to theme the sidebar. + +```css +@layer base { + :root { + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 0 0% 98%; + --sidebar-primary-foreground: 240 5.9% 10%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} +``` + +**We intentionally use different variables for the sidebar and the rest of the application** to make it easy to have a sidebar that is styled differently from the rest of the application. Think a sidebar with a darker shade from the main application. + +## Styling + +Here are some tips for styling the sidebar based on different states. + +- **Styling an element based on the sidebar collapsible state.** The following will hide the `SidebarGroup` when the sidebar is in `icon` mode. + +```tsx + + + + + +``` + +- **Styling a menu action based on the menu button active state.** The following will force the menu action to be visible when the menu button is active. + +```tsx + + + + +``` + +You can find more tips on using states for styling in this [Twitter thread](https://x.com/shadcn/status/1842329158879420864). + +## Changelog + +### 2024-10-30 Cookie handling in setOpen + +- [#5593](https://github.com/shadcn-ui/ui/pull/5593) - Improved setOpen callback logic in ``. + +Update the `setOpen` callback in `` as follows: + +```tsx showLineNumbers +const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === 'function' ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] +) +``` + +### 2024-10-21 Fixed `text-sidebar-foreground` + +- [#5491](https://github.com/shadcn-ui/ui/pull/5491) - Moved `text-sidebar-foreground` from `` to `` component. + +### 2024-10-20 Typo in `useSidebar` hook. + +Fixed typo in `useSidebar` hook. + +```diff showLineNumbers title="sidebar.tsx" +- throw new Error("useSidebar must be used within a Sidebar.") ++ throw new Error("useSidebar must be used within a SidebarProvider.") +``` diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 166fe56b8d7..37bf296a2ae 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -250,6 +250,7 @@ export * from './src/components/radio-group-card' export * from './src/components/TreeView' export * from './src/components/shadcn/ui/chart' +export * from './src/components/shadcn/ui/sidebar' // links diff --git a/packages/ui/src/components/shadcn/hooks/useMobile.ts b/packages/ui/src/components/shadcn/hooks/useMobile.ts new file mode 100644 index 00000000000..4331d5c5620 --- /dev/null +++ b/packages/ui/src/components/shadcn/hooks/useMobile.ts @@ -0,0 +1,19 @@ +import * as React from 'react' + +const MOBILE_BREAKPOINT = 768 + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState(undefined) + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + } + mql.addEventListener('change', onChange) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + return () => mql.removeEventListener('change', onChange) + }, []) + + return !!isMobile +} diff --git a/packages/ui/src/components/shadcn/ui/sidebar.tsx b/packages/ui/src/components/shadcn/ui/sidebar.tsx new file mode 100644 index 00000000000..77aea8b0612 --- /dev/null +++ b/packages/ui/src/components/shadcn/ui/sidebar.tsx @@ -0,0 +1,740 @@ +'use client' + +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { VariantProps, cva } from 'class-variance-authority' +import { PanelLeft } from 'lucide-react' + +import { useIsMobile } from '../hooks/useMobile' +import { cn } from './../../../lib/utils' +import { Button } from './button' +import { Input } from './input' +import { Separator } from './separator' +import { Sheet, SheetContent } from './sheet' +import { Skeleton } from './skeleton' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip' + +const SIDEBAR_COOKIE_NAME = 'sidebar:state' +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = '13rem' +const SIDEBAR_WIDTH_MOBILE = '18rem' +const SIDEBAR_WIDTH_ICON = '3rem' +const SIDEBAR_KEYBOARD_SHORTCUT = '[' + +type SidebarContext = { + state: 'expanded' | 'collapsed' + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error('useSidebar must be used within a SidebarProvider.') + } + + return context +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref + ) => { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === 'function' ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? 'expanded' : 'collapsed' + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + + +
    + {children} +
    +
    +
    + ) + } +) +SidebarProvider.displayName = 'SidebarProvider' + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> & { + side?: 'left' | 'right' + variant?: 'sidebar' | 'floating' | 'inset' + collapsible?: 'offcanvas' | 'icon' | 'none' + } +>( + ( + { + side = 'left', + variant = 'sidebar', + collapsible = 'offcanvas', + className, + children, + ...props + }, + ref + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === 'none') { + return ( +
    + {children} +
    + ) + } + + if (isMobile) { + return ( + + +
    {children}
    +
    +
    + ) + } + + return ( +
    + {/* This is what handles the sidebar gap on desktop */} +
    + +
    + ) + } +) +Sidebar.displayName = 'Sidebar' + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + + ) +}) +SidebarTrigger.displayName = 'SidebarTrigger' + +const SidebarRail = React.forwardRef>( + ({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + -
    - - + +

    The public schema for this project is not exposed

    +

    + You will not be able to query tables and views in the public schema via supabase-js or HTTP + clients. Configure this behavior in your project's Data API settings. +

    + +
    ) } From 4dcd9f36c142378ae7ec4aee949ab1f8f18a30c5 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 10 Feb 2025 17:54:43 +0800 Subject: [PATCH 10/28] Render GH profile image in NavigationBar if user is GH (#33476) * Render GH profile image in NavigationBar if user is GH * Small refactor * Clean up --- .../Organization/TeamSettings/MemberRow.tsx | 39 +++++++++---------- .../NavigationBar/MobileNavigationBar.tsx | 12 ++---- .../NavigationBar/MobileViewNav.tsx | 3 -- .../NavigationBar/NavigationBar.tsx | 20 +++++----- apps/studio/components/ui/ProfileImage.tsx | 35 +++++++++++++++++ apps/studio/data/profile/types.ts | 4 +- apps/studio/lib/github.tsx | 4 ++ apps/studio/lib/profile.tsx | 5 ++- 8 files changed, 79 insertions(+), 43 deletions(-) create mode 100644 apps/studio/components/ui/ProfileImage.tsx diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx index af4c964549e..dc8b94ea948 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx @@ -1,13 +1,13 @@ import { ArrowRight, Check, Minus, User, X } from 'lucide-react' -import Image from 'next/legacy/image' -import { useState } from 'react' import Table from 'components/to-be-cleaned/Table' import PartnerIcon from 'components/ui/PartnerIcon' +import { ProfileImage } from 'components/ui/ProfileImage' import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' import { OrganizationMember } from 'data/organizations/organization-members-query' import { useProjectsQuery } from 'data/projects/projects-query' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { getGitHubProfileImgUrl } from 'lib/github' import { useProfile } from 'lib/profile' import Link from 'next/link' import { @@ -33,7 +33,6 @@ const MEMBER_ORIGIN_TO_MANAGED_BY = { export const MemberRow = ({ member }: MemberRowProps) => { const { profile } = useProfile() const selectedOrganization = useSelectedOrganization() - const [hasInvalidImg, setHasInvalidImg] = useState(false) const { data: projects } = useProjectsQuery() const { data: roles, isLoading: isLoadingRoles } = useOrganizationRolesV2Query({ @@ -47,6 +46,9 @@ export const MemberRow = ({ member }: MemberRowProps) => { const isEmailUser = member.username === member.primary_email const isFlyUser = Boolean(member.primary_email?.endsWith('customer.fly.io')) + const profileImageUrl = + isInvitedUser || isEmailUser || isFlyUser ? undefined : getGitHubProfileImgUrl(member.username) + // [Joshen] From project role POV, mask any roles for other projects const isObfuscated = member.role_ids.filter((id) => { @@ -69,25 +71,22 @@ export const MemberRow = ({ member }: MemberRowProps) => { return ( -
    -
    - {isInvitedUser || isEmailUser || isFlyUser || hasInvalidImg ? ( -
    +
    +
    - ) : ( - {member.username} { - setHasInvalidImg(true) - }} - /> - )} -
    + } + />
    {isInvitedUser === undefined ? (

    {member.primary_email}

    diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileNavigationBar.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileNavigationBar.tsx index efd1014ddae..8464ac57d6b 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileNavigationBar.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileNavigationBar.tsx @@ -1,19 +1,15 @@ +import { Menu, Search } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' -import { Search, Menu } from 'lucide-react' +import { useState } from 'react' import { useParams } from 'common' import { IS_PLATFORM } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' - -import { buttonVariants, cn, Sheet, SheetContent } from 'ui' +import { buttonVariants, cn } from 'ui' import { CommandMenuTrigger } from 'ui-patterns' -import { NavContent } from './NavigationBar' -import { useState } from 'react' import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav' - -export const ICON_SIZE = 20 -export const ICON_STROKE_WIDTH = 1.5 +import { NavContent } from './NavigationBar' const MobileNavigationBar = () => { const router = useRouter() diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileViewNav.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileViewNav.tsx index b1f0a90e730..71dd9e7d3e0 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileViewNav.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/MobileViewNav.tsx @@ -2,9 +2,6 @@ import { PropsWithChildren } from 'react' import { useAppStateSnapshot } from 'state/app-state' import { cn } from 'ui' -export const ICON_SIZE = 20 -export const ICON_STROKE_WIDTH = 1.5 - interface Props { title?: string } diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index 1553d82a25e..40ae286e050 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -1,4 +1,4 @@ -import { Home, User } from 'icons' +import { Home } from 'icons' import { isUndefined } from 'lodash' import { Command, FileText, FlaskConical, Search, Settings } from 'lucide-react' import { useTheme } from 'next-themes' @@ -8,6 +8,7 @@ import { useState } from 'react' import { useParams } from 'common' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { ProfileImage } from 'components/ui/ProfileImage' import { useProjectLintsQuery } from 'data/lint/lint-query' import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' @@ -58,8 +59,6 @@ export const ICON_STROKE_WIDTH = 1.5 const NavigationBar = () => { const snap = useAppStateSnapshot() - const [userDropdownOpen, setUserDropdownOpenState] = useState(false) - const [storedAllowNavPanel] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.EXPAND_NAVIGATION_PANEL, true @@ -78,9 +77,7 @@ const NavigationBar = () => { 'hide-scrollbar flex flex-col justify-between overflow-y-auto' )} onMouseEnter={() => allowNavPanelToExpand && snap.setNavigationPanelOpen(true)} - onMouseLeave={() => { - if (!userDropdownOpen && allowNavPanelToExpand) snap.setNavigationPanelOpen(false) - }} + onMouseLeave={() => allowNavPanelToExpand && snap.setNavigationPanelOpen(false)} > @@ -100,6 +97,7 @@ export const NavContent = () => { const signOut = useSignOut() const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled() + const [hasInvalidImg, setHasInvalidImg] = useState(false) const [userDropdownOpen, setUserDropdownOpenState] = useState(false) const [storedAllowNavPanel] = useLocalStorageQuery( @@ -189,17 +187,19 @@ export const NavContent = () => { const UserAccountButton = (