diff --git a/apps/design-system/README.md b/apps/design-system/README.md index e8db8fd524659..96a8eaf8fefd0 100644 --- a/apps/design-system/README.md +++ b/apps/design-system/README.md @@ -2,30 +2,48 @@ Design resources for building consistent user experiences at Supabase. -## Getting Started +## Getting started -First, make a copy of _.env.local.example_ and name it _env.local_. Then run the development server as described below. +First, make a copy of _.env.local.example_ and name it _env.local_. Then install any required packages and start the development server: -From within this _design-system_ directory, run: +```bash +cd apps/design-system +pnpm i +pnpm dev:full +``` + +The `dev:full` command runs both the Next.js development server and Contentlayer concurrently, which is recommended for most development workflows. + +### Alternative commands + +You can also run the development server and content watcher separately: ```bash +# Run only the Next.js development server pnpm dev + +# Run only the content watcher (in a separate terminal shell) +pnpm content:dev ``` -Or from the root directory run: +Or run the development server from the root directory: ```bash pnpm dev:design-system ``` -Open [http://localhost:3003](http://localhost:3003) with your browser to see the result. - -### Watching for MDX changes +To run both the development server and content watcher from the root directory, you can use: -If you would like to watch for changes to MDX files with hot reload, you can run the following command in a separate terminal shell: +```bash +# Run the development server +pnpm dev:design-system +# Run the content watcher (in a separate terminal shell) +pnpm --filter=design-system content:dev ``` -pnpm content:dev -``` -This runs Contentlayer concurrently and watches for any changes. +Open [http://localhost:3003](http://localhost:3003) in your browser to see the result. + +### Watching for MDX changes + +The `dev:full` command automatically watches for changes to MDX files with hot reload. If you're running the `pnpm dev` separately, you'll need to run `pnpm content:dev` in a separate terminal shell to watch for content changes. diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index ea8252799c775..912c60fe77cd3 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -918,6 +918,39 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "nav-menu-demo": { + name: "nav-menu-demo", + type: "components:example", + registryDependencies: ["nav-menu"], + component: React.lazy(() => import("@/registry/default/example/nav-menu-demo")), + source: "", + files: ["registry/default/example/nav-menu-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "nav-menu-badges": { + name: "nav-menu-badges", + type: "components:example", + registryDependencies: ["nav-menu"], + component: React.lazy(() => import("@/registry/default/example/nav-menu-badges")), + source: "", + files: ["registry/default/example/nav-menu-badges.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "nav-menu-icons": { + name: "nav-menu-icons", + type: "components:example", + registryDependencies: ["nav-menu"], + component: React.lazy(() => import("@/registry/default/example/nav-menu-icons")), + source: "", + files: ["registry/default/example/nav-menu-icons.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "popover-demo": { name: "popover-demo", type: "components:example", diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts index cd0c0bf8e2cb0..c41034a9f2f22 100644 --- a/apps/design-system/config/docs.ts +++ b/apps/design-system/config/docs.ts @@ -69,6 +69,16 @@ export const docsConfig: DocsConfig = { }, ], }, + { + title: 'UI Patterns', + items: [ + { + title: 'Navigation', + href: '/docs/ui-patterns/navigation', + items: [], + }, + ], + }, { title: 'Fragment Components', items: [ @@ -289,6 +299,11 @@ export const docsConfig: DocsConfig = { href: '/docs/components/menubar', items: [], }, + { + title: 'NavMenu', + href: '/docs/components/nav-menu', + items: [], + }, { title: 'Navigation Menu', href: '/docs/components/navigation-menu', diff --git a/apps/design-system/content/docs/changelog.mdx b/apps/design-system/content/docs/changelog.mdx index bd19ae0967b91..a96a3e675f167 100644 --- a/apps/design-system/content/docs/changelog.mdx +++ b/apps/design-system/content/docs/changelog.mdx @@ -4,6 +4,14 @@ description: Latest updates and announcements. toc: false --- +## 2nd Oct 2025 - Update button styles + +- UI Patterns section added for higher-level guidance +- [`Navigation`](/design-system/docs/ui-patterns/navigation) UI pattern added +- [`NavMenu`](/design-system/docs/components/nav-menu) component added + +[PR](https://github.com/supabase/supabase/pull/39188) + ## 1st Sep 2025 - Update button styles - [`Color usage`](/design-system/docs/color-usage) page updated diff --git a/apps/design-system/content/docs/components/nav-menu.mdx b/apps/design-system/content/docs/components/nav-menu.mdx new file mode 100644 index 0000000000000..b6ed65529b3af --- /dev/null +++ b/apps/design-system/content/docs/components/nav-menu.mdx @@ -0,0 +1,57 @@ +--- +title: NavMenu +description: A horizontal list of related views within a consistent context. +component: true +--- + + + This component is titled very similarly to the [Navigation Menu](../components/navigation-menu) + component. Consider renaming it to something like TabMenu or + [UnderlineNav](https://primer.style/product/components/underline-nav/). + + + + +## Usage + +NavBar is exclusively used as sub-navigation within PageLayout. + +```tsx +import { NavMenu, NavMenuItem } from 'ui' +``` + +```tsx + + + Overview + + + Documentation + + +``` + +## Examples + +### With counter badges + +You may include counter badges to indicate the number of items within a tab, for example the amount of buckets in storage. + + + +### With icons + +You may include icons to more clearly represent the content of each tab. Every tab should have an icon (or no icon). + + diff --git a/apps/design-system/content/docs/icons.mdx b/apps/design-system/content/docs/icons.mdx index f9df03365a43f..5e020a680d436 100644 --- a/apps/design-system/content/docs/icons.mdx +++ b/apps/design-system/content/docs/icons.mdx @@ -11,14 +11,108 @@ description: Icons make actions and navigation across Supabase easier. ## Tints -Destructive actions, such as deleting an API key, don’t need to be [tinted](color-usage#text) with `text-destructive` because there should be a confirmation dialog as a failsafe right after. +Use classes just like you would for [text](color-usage#text) to tint icons. For example: -## UI Icons +```jsx + +``` -We rely on Lucide icons for most of our UI icons. +Just like text, don’t tint icons with `text-destructive` for destructive actions. There should be a confirmation dialog right after which can handle the destructive styling. -## Custom Icons +## UI icons -Tap on an icon below to copy the JSX, SVG, or import path. +We rely on [Lucide](https://lucide.dev/icons/) for any standard UI icon needs. + +## Custom icons + +Create and use custom icons when Lucide doesn’t have the icon you need. Tap on an icon below to copy the JSX, SVG, or import path. + +### Usage + +```jsx +import { ReplaceCode, InsertCode, BucketAdd } from 'icons' + +function app() { + return ( + <> + + + + + ) +} +``` + +**Default props**: All icons have `strokeWidth={2}` and `size={24}` by default. Override these props as needed for your use case. + +### Adding new custom icons + +To add a new custom icon to the Supabase icon library: + +1. **Create SVG file**: Add your SVG file to `packages/icons/src/raw-icons/` with a kebab-case name (e.g., `my-new-icon.svg`). Make sure it has follows these exact requirements: + + - Exported at 24x24px with `viewBox="0 0 24 24"` + - Uses `stroke="currentColor"` for strokes (no hardcoded colors) + - Uses `fill="none"` for fills (no hardcoded colors) + - Icon content is optically centered and around 18x18px within the 24x24 frame + - Any unnecessary elements like ``, ``, and `` wrappers have been removed + - SVG structure is as simple as possible with just `` elements + +Just leave attributes like `stroke-width` as they are. The conversion to camel-case (for React compatibility) is handled by the below build process. + +2. **Build the component**: Run `npm run build:icons` from inside the `packages/icons` directory + +3. **Use the icon**: Import and use like any other icon: + + ```jsx + import { MyNewIcon } from 'icons' + ; + ``` + +### SVG design guidelines + +Icons should: + +- Always be exported 24x24px +- Have an icon inside that frame that’s around 18x18px(ish) +- Use clean, simple paths without unnecessary wrapper elements + +#### Bad example ❌ + +Notice the hardcoded colors, unnecessary backgrounds, and complex structure: + +```svg + + + + + +``` + +#### Good example ✅ + +Clean structure with `currentColor` and proper attributes: + +```svg + + + + + +``` + +{/* This is still wrong: */} + +```svg + + + + + +``` + +### Troubleshooting + +If your SVG specifies `stroke-width` attributes, they will override the component's `strokeWidth` prop. Remove stroke attributes from individual paths to let the component control them. diff --git a/apps/design-system/content/docs/ui-patterns/navigation.mdx b/apps/design-system/content/docs/ui-patterns/navigation.mdx new file mode 100644 index 0000000000000..a0fc17e816f3e --- /dev/null +++ b/apps/design-system/content/docs/ui-patterns/navigation.mdx @@ -0,0 +1,14 @@ +--- +title: Navigation +description: Navigation patterns help users understand where they are and where they can go next. +--- + +Supabase has a necessarily complex navigation system to handle multiple products and levels of hierarchy. This page introduces those general patterns, best practices, and the components involved. + +## Components + +### NavMenu + +A horizontal list of related views within a consistent PageLayout context, allowing for clearer page-level organisation. Activating a NavMenu item should trigger a URL change. + +[NavMenu component guidelines](../components/nav-menu) diff --git a/apps/design-system/package.json b/apps/design-system/package.json index cd51dbaaf6bc1..daeefc6cec0d0 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -6,6 +6,7 @@ "scripts": { "preinstall": "npx only-allow pnpm", "dev": "next dev --turbopack --port 3003", + "dev:full": "concurrently \"pnpm dev\" \"pnpm content:dev\"", "build": "pnpm run content:build && pnpm run build:registry && next build --turbopack", "build:registry": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache", "start": "next start", @@ -53,6 +54,7 @@ "@types/react": "catalog:", "@types/react-dom": "catalog:", "config": "workspace:^", + "concurrently": "^8.2.2", "mdast-util-toc": "^6.1.1", "postcss": "^8.5.3", "rimraf": "^4.1.3", diff --git a/apps/design-system/registry/default/example/nav-menu-badges.tsx b/apps/design-system/registry/default/example/nav-menu-badges.tsx new file mode 100644 index 0000000000000..c5a6603218519 --- /dev/null +++ b/apps/design-system/registry/default/example/nav-menu-badges.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link' +import { Badge, NavMenu, NavMenuItem } from 'ui' + +export default function NavMenuWithIcons() { + return ( + + + + Buckets + 10 + + + + + Policies 2 + + + + + Settings + + + + ) +} diff --git a/apps/design-system/registry/default/example/nav-menu-demo.tsx b/apps/design-system/registry/default/example/nav-menu-demo.tsx new file mode 100644 index 0000000000000..e952c3d2cd7ae --- /dev/null +++ b/apps/design-system/registry/default/example/nav-menu-demo.tsx @@ -0,0 +1,21 @@ +import Link from 'next/link' +import { NavMenu, NavMenuItem } from 'ui' + +export default function NavMenuDemo() { + return ( + + + Overview + + + Invocations + + + Logs + + + Code + + + ) +} diff --git a/apps/design-system/registry/default/example/nav-menu-icons.tsx b/apps/design-system/registry/default/example/nav-menu-icons.tsx new file mode 100644 index 0000000000000..86ef4549ac441 --- /dev/null +++ b/apps/design-system/registry/default/example/nav-menu-icons.tsx @@ -0,0 +1,29 @@ +import Link from 'next/link' +import { ArchiveIcon, ShieldIcon, SettingsIcon } from 'lucide-react' +import { Badge, NavMenu, NavMenuItem } from 'ui' + +export default function NavMenuWithIcons() { + return ( + + + + + Buckets + 10 + + + + + + Policies + + + + + + Settings + + + + ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index b79b6322d4496..4e389b1d238be 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -575,6 +575,24 @@ export const examples: Registry = [ registryDependencies: ['navigation-menu'], files: ['example/navigation-menu-responsive.tsx'], }, + { + name: 'nav-menu-demo', + type: 'components:example', + registryDependencies: ['nav-menu'], + files: ['example/nav-menu-demo.tsx'], + }, + { + name: 'nav-menu-badges', + type: 'components:example', + registryDependencies: ['nav-menu'], + files: ['example/nav-menu-badges.tsx'], + }, + { + name: 'nav-menu-icons', + type: 'components:example', + registryDependencies: ['nav-menu'], + files: ['example/nav-menu-icons.tsx'], + }, // { // name: 'pagination-demo', // type: 'components:example', diff --git a/apps/design-system/styles/mdx.css b/apps/design-system/styles/mdx.css index afac3b50ede14..84391167b6f4d 100644 --- a/apps/design-system/styles/mdx.css +++ b/apps/design-system/styles/mdx.css @@ -15,7 +15,7 @@ } [data-rehype-pretty-code-fragment] { - @apply relative text-white; + @apply relative; } [data-rehype-pretty-code-fragment] code { diff --git a/apps/docs/components/RegionsList.tsx b/apps/docs/components/RegionsList.tsx index 0f9826046b28b..15f751bb1cce2 100644 --- a/apps/docs/components/RegionsList.tsx +++ b/apps/docs/components/RegionsList.tsx @@ -1,4 +1,5 @@ import { AWS_REGIONS } from 'shared-data' +import { SMART_REGION_TO_EXACT_REGION_MAP } from 'shared-data/regions' export function RegionsList() { return ( @@ -12,3 +13,15 @@ export function RegionsList() { ) } +export function SmartRegionsList() { + return ( +
    + {[...SMART_REGION_TO_EXACT_REGION_MAP.entries()].map(([smartRegion, exactRegion]) => ( +
  • + {smartRegion} + {exactRegion} +
  • + ))} +
+ ) +} diff --git a/apps/docs/content/guides/platform/regions.mdx b/apps/docs/content/guides/platform/regions.mdx index 5085bb72faab3..594e3f80cbf23 100644 --- a/apps/docs/content/guides/platform/regions.mdx +++ b/apps/docs/content/guides/platform/regions.mdx @@ -5,6 +5,14 @@ subtitle: Spin up Supabase projects in our global regions The following regions are available for your Supabase projects. +## Smart region selection + +Smart Region Selection is a feature that allows you to select from a list of broader regions. Optimizations are made to ensure that the selected region has compute capacity availability. + + + +Currently, smart region selection is not available for read replicas and project-related operations via the management API. + ## AWS diff --git a/apps/docs/features/docs/MdxBase.shared.tsx b/apps/docs/features/docs/MdxBase.shared.tsx index 2e1fef8edc4b4..e332d72fd8a52 100644 --- a/apps/docs/features/docs/MdxBase.shared.tsx +++ b/apps/docs/features/docs/MdxBase.shared.tsx @@ -18,7 +18,7 @@ import { NavData } from '~/components/NavData' import { Price } from '~/components/Price' import { ProjectConfigVariables } from '~/components/ProjectConfigVariables' import { RealtimeLimitsEstimator } from '~/components/RealtimeLimitsEstimator' -import { RegionsList } from '~/components/RegionsList' +import { RegionsList, SmartRegionsList } from '~/components/RegionsList' import { SharedData } from '~/components/SharedData' import StepHikeCompact from '~/components/StepHikeCompact' import { CodeSampleDummy, CodeSampleWrapper } from '~/features/directives/CodeSample.client' @@ -60,6 +60,7 @@ const components = { ProjectConfigVariables, RealtimeLimitsEstimator, RegionsList, + SmartRegionsList, SharedData, ShowUntil, SqlToRest, diff --git a/apps/docs/next-env.d.ts b/apps/docs/next-env.d.ts new file mode 100644 index 0000000000000..830fb594ca297 --- /dev/null +++ b/apps/docs/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/studio/components/interfaces/BranchManagement/ReviewWithAI.tsx b/apps/studio/components/interfaces/BranchManagement/ReviewWithAI.tsx index 9a93e5eaac335..fe63f11d8ddad 100644 --- a/apps/studio/components/interfaces/BranchManagement/ReviewWithAI.tsx +++ b/apps/studio/components/interfaces/BranchManagement/ReviewWithAI.tsx @@ -1,9 +1,9 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { Branch } from 'data/branches/branches-query' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { useTablesQuery } from 'data/tables/tables-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { useProjectByRefQuery } from 'hooks/misc/useSelectedProject' import { tablesToSQL } from 'lib/helpers' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { AiIconAnimation } from 'ui' @@ -28,7 +28,7 @@ export const ReviewWithAI = ({ const { mutate: sendEvent } = useSendEventMutation() // Get parent project for production schema - const { data: parentProject } = useProjectByRefQuery(parentProjectRef) + const { data: parentProject } = useProjectDetailQuery({ ref: parentProjectRef }) // Fetch production schema tables const { data: productionTables } = useTablesQuery( diff --git a/apps/studio/components/interfaces/Home/Home.tsx b/apps/studio/components/interfaces/Home/Home.tsx index 555019324c8de..a2caf9d860be2 100644 --- a/apps/studio/components/interfaces/Home/Home.tsx +++ b/apps/studio/components/interfaces/Home/Home.tsx @@ -16,16 +16,13 @@ import { InlineLink } from 'components/ui/InlineLink' import { ProjectUpgradeFailedBanner } from 'components/ui/ProjectUpgradeFailedBanner' import { useBranchesQuery } from 'data/branches/branches-query' import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useTablesQuery } from 'data/tables/tables-query' import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { - useIsOrioleDb, - useProjectByRefQuery, - useSelectedProjectQuery, -} from 'hooks/misc/useSelectedProject' +import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL, IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import { @@ -44,7 +41,7 @@ import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' export const Home = () => { const { data: project } = useSelectedProjectQuery() const { data: organization } = useSelectedOrganizationQuery() - const { data: parentProject } = useProjectByRefQuery(project?.parent_project_ref) + const { data: parentProject } = useProjectDetailQuery({ ref: project?.parent_project_ref }) const isOrioleDb = useIsOrioleDb() const snap = useAppStateSnapshot() const { ref, enableBranching } = useParams() diff --git a/apps/studio/components/interfaces/Home/ProjectUsage.tsx b/apps/studio/components/interfaces/Home/ProjectUsage.tsx index 1ee9a4ddd10b9..346332575d81a 100644 --- a/apps/studio/components/interfaces/Home/ProjectUsage.tsx +++ b/apps/studio/components/interfaces/Home/ProjectUsage.tsx @@ -1,6 +1,7 @@ import dayjs from 'dayjs' import sumBy from 'lodash/sumBy' -import { Archive, ChevronDown, Database, Key, Zap } from 'lucide-react' +import { ChevronDown } from 'lucide-react' +import { Auth, Database, Realtime, Storage } from 'icons' import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' @@ -200,7 +201,7 @@ const ProjectUsage = () => { - + } title="Database" @@ -226,7 +227,7 @@ const ProjectUsage = () => { - + } title="Auth" @@ -252,7 +253,7 @@ const ProjectUsage = () => { - + } title="Storage" @@ -278,7 +279,7 @@ const ProjectUsage = () => { - + } title="Realtime" diff --git a/apps/studio/components/interfaces/HomeNew/Home.tsx b/apps/studio/components/interfaces/HomeNew/Home.tsx index 36868977f6f4b..4e2baa81de544 100644 --- a/apps/studio/components/interfaces/HomeNew/Home.tsx +++ b/apps/studio/components/interfaces/HomeNew/Home.tsx @@ -3,18 +3,15 @@ import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-ki import { useEffect, useRef } from 'react' import { IS_PLATFORM, useParams } from 'common' -import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { SortableSection } from 'components/interfaces/HomeNew/SortableSection' import { TopSection } from 'components/interfaces/HomeNew/TopSection' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import { useBranchesQuery } from 'data/branches/branches-query' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useLocalStorage } from 'hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { - useIsOrioleDb, - useProjectByRefQuery, - useSelectedProjectQuery, -} from 'hooks/misc/useSelectedProject' +import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import { AdvisorSection } from './AdvisorSection' @@ -31,7 +28,7 @@ export const HomeV2 = () => { const snap = useAppStateSnapshot() const { data: project } = useSelectedProjectQuery() const { data: organization } = useSelectedOrganizationQuery() - const { data: parentProject } = useProjectByRefQuery(project?.parent_project_ref) + const { data: parentProject } = useProjectDetailQuery({ ref: project?.parent_project_ref }) const { mutate: sendEvent } = useSendEventMutation() const hasShownEnableBranchingModalRef = useRef(false) diff --git a/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx b/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx index 67f46bdbb204e..b29c45a6761bb 100644 --- a/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx +++ b/apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx @@ -1,5 +1,5 @@ import dayjs from 'dayjs' -import { Archive, ChevronDown, Code, Database, Key, Zap } from 'lucide-react' +import { ChevronDown } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useMemo, useState } from 'react' @@ -77,7 +77,6 @@ type ServiceKey = 'db' | 'functions' | 'auth' | 'storage' | 'realtime' type ServiceEntry = { key: ServiceKey title: string - icon: React.ReactNode href?: string route: string enabled: boolean @@ -159,7 +158,7 @@ export const ProjectUsageSection = () => { { key: 'db', title: 'Database requests', - icon: , + href: `/project/${projectRef}/editor`, route: '/logs/postgres-logs', enabled: true, @@ -167,14 +166,12 @@ export const ProjectUsageSection = () => { { key: 'functions', title: 'Functions requests', - icon: , route: '/logs/edge-functions-logs', enabled: true, }, { key: 'auth', title: 'Auth requests', - icon: , href: `/project/${projectRef}/auth/users`, route: '/logs/auth-logs', enabled: authEnabled, @@ -182,7 +179,6 @@ export const ProjectUsageSection = () => { { key: 'storage', title: 'Storage requests', - icon: , href: `/project/${projectRef}/storage/buckets`, route: '/logs/storage-logs', enabled: storageEnabled, @@ -190,7 +186,6 @@ export const ProjectUsageSection = () => { { key: 'realtime', title: 'Realtime requests', - icon: , route: '/logs/realtime-logs', enabled: true, }, diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index 66262155e9587..ed63dd7ac53b5 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -8,21 +8,15 @@ import { ScaffoldContainer, ScaffoldHeader, ScaffoldTitle } from 'components/lay import AlertError from 'components/ui/AlertError' import DateRangePicker from 'components/ui/DateRangePicker' import NoPermission from 'components/ui/NoPermission' +import { OrganizationProjectSelector } from 'components/ui/OrganizationProjectSelector' import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useProjectsQuery } from 'data/projects/projects-query' +import { OrgProject } from 'data/projects/org-projects-infinite-query' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants/metrics' -import { - cn, - Select_Shadcn_, - SelectContent_Shadcn_, - SelectGroup_Shadcn_, - SelectItem_Shadcn_, - SelectTrigger_Shadcn_, - SelectValue_Shadcn_, -} from 'ui' +import { Check, ChevronDown } from 'lucide-react' +import { Button, cn, CommandGroup_Shadcn_, CommandItem_Shadcn_ } from 'ui' import { Admonition } from 'ui-patterns' import { Restriction } from '../BillingSettings/Restriction' import Activity from './Activity' @@ -31,12 +25,19 @@ import Egress from './Egress' import SizeAndCounts from './SizeAndCounts' import { TotalUsage } from './TotalUsage' +// [Joshen] JFYI this component could use nuqs to handle `projectRef` state which will help +// simplify some of the implementation here. + export const Usage = () => { const { slug, projectRef } = useParams() + const [dateRange, setDateRange] = useState() + const [selectedProject, setSelectedProject] = useState() + const [selectedProjectRefInputValue, setSelectedProjectRefInputValue] = useState< string | undefined >('all-projects') + const [openProjectSelector, setOpenProjectSelector] = useState(false) // [Alaister] 'all-projects' is not a valid project ref, it's just used as an extra // state for the select input. As such we need to remove it for the selected project ref @@ -48,8 +49,10 @@ export const Usage = () => { 'stripe.subscriptions' ) - const { data: organization } = useSelectedOrganizationQuery() - const { data, isSuccess } = useProjectsQuery() + const { isSuccess: isSuccessProjectDetail } = useProjectDetailQuery({ + ref: selectedProjectRef, + }) + const { data: subscription, error: subscriptionError, @@ -58,10 +61,6 @@ export const Usage = () => { isSuccess: isSuccessSubscription, } = useOrgSubscriptionQuery({ orgSlug: slug }) - const orgProjects = (data?.projects ?? []).filter( - (project) => project.organization_id === organization?.id - ) - const billingCycleStart = useMemo(() => { return dayjs.unix(subscription?.current_period_start ?? 0).utc() }, [subscription]) @@ -105,19 +104,13 @@ export const Usage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dateRange, subscription]) - const selectedProject = selectedProjectRef - ? orgProjects?.find((it) => it.ref === selectedProjectRef) - : undefined - useEffect(() => { - if (projectRef && isSuccess && orgProjects !== undefined) { - if (orgProjects.find((project) => project.ref === projectRef)) { - setSelectedProjectRefInputValue(projectRef) - } + if (projectRef && isSuccessProjectDetail) { + setSelectedProjectRefInputValue(projectRef) } // [Joshen] Since we're already looking at isSuccess // eslint-disable-next-line react-hooks/exhaustive-deps - }, [projectRef, isSuccess]) + }, [projectRef, isSuccessProjectDetail]) return ( <> @@ -162,37 +155,60 @@ export const Usage = () => { className="!w-48" /> - { - if (value === 'all-projects') setSelectedProjectRefInputValue('all-projects') - else setSelectedProjectRefInputValue(value) + { + setSelectedProject(project) + setSelectedProjectRefInputValue(project.ref) }} - > - - - - - - { + return ( + + ) + }} + renderRow={(project) => { + const isSelected = selectedProjectRefInputValue === project.ref + return ( +
+ {project.name} - - ))} - - - + + {isSelected && } +
+ ) + }} + renderActions={() => ( + + { + setOpenProjectSelector(false) + setSelectedProjectRefInputValue('all-projects') + }} + onClick={() => { + setOpenProjectSelector(false) + setSelectedProjectRefInputValue('all-projects') + }} + > + All projects + {selectedProjectRefInputValue === 'all-projects' && } + + + )} + />
diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts index 64060ffc0b72d..1c2da89ce34b9 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts @@ -1,14 +1,9 @@ import type { CloudProvider, Region } from 'shared-data' import { AWS_REGIONS, FLY_REGIONS } from 'shared-data' - -const smartRegionToExactRegionMap = new Map([ - ['Americas', 'East US (North Virginia)'], - ['Europe', 'Central EU (Frankfurt)'], - ['APAC', 'Southeast Asia (Singapore)'], -]) +import { SMART_REGION_TO_EXACT_REGION_MAP } from 'shared-data/regions' export function smartRegionToExactRegion(smartOrExactRegion: string) { - return smartRegionToExactRegionMap.get(smartOrExactRegion) ?? smartOrExactRegion + return SMART_REGION_TO_EXACT_REGION_MAP.get(smartOrExactRegion) ?? smartOrExactRegion } export function getAvailableRegions(cloudProvider: CloudProvider): Region { diff --git a/apps/studio/components/interfaces/Reports/v2/ReportChartUpsell.tsx b/apps/studio/components/interfaces/Reports/v2/ReportChartUpsell.tsx index d5bd366bf0018..77bda75ecc7cc 100644 --- a/apps/studio/components/interfaces/Reports/v2/ReportChartUpsell.tsx +++ b/apps/studio/components/interfaces/Reports/v2/ReportChartUpsell.tsx @@ -1,24 +1,23 @@ -import { LogChartHandler } from 'components/ui/Charts/LogChartHandler' import Link from 'next/link' import { useRef, useState } from 'react' +import { LazyComposedChartHandler } from 'components/ui/Charts/ComposedChartHandler' import { Button, Card, cn } from 'ui' -export function ReportChartUpsell({ - report, - orgSlug, -}: { +interface ReportsChartUpsellProps { report: { label: string availableIn: string[] } orgSlug: string -}) { - const [isHoveringUpgrade, setIsHoveringUpgrade] = useState(false) +} +export const ReportChartUpsell = ({ report, orgSlug }: ReportsChartUpsellProps) => { const startDate = '2025-01-01' const endDate = '2025-01-02' + const [isHoveringUpgrade, setIsHoveringUpgrade] = useState(false) + const getExpDemoChartData = () => new Array(20).fill(0).map((_, index) => ({ period_start: new Date(startDate).getTime() + index * 1000, @@ -65,7 +64,7 @@ export function ReportChartUpsell({
- { const { data: selectedOrg } = useSelectedOrganizationQuery() const { data: selectedProject, isLoading: isLoadingProject } = useSelectedProjectQuery() - const { data: parentProject } = useProjectByRefQuery(selectedProject?.parent_project_ref) + const { data: parentProject } = useProjectDetailQuery({ + ref: selectedProject?.parent_project_ref, + }) const isBranch = parentProject !== undefined const { data: settings } = useProjectSettingsV2Query({ projectRef }) diff --git a/apps/studio/components/interfaces/Settings/General/General.tsx b/apps/studio/components/interfaces/Settings/General/General.tsx index 46d19f4d58879..61c0f7d22364a 100644 --- a/apps/studio/components/interfaces/Settings/General/General.tsx +++ b/apps/studio/components/interfaces/Settings/General/General.tsx @@ -8,11 +8,12 @@ import { FormPanel } from 'components/ui/Forms/FormPanel' import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection' import Panel from 'components/ui/Panel' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { useProjectUpdateMutation } from 'data/projects/project-update-mutation' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { useProjectByRefQuery, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, @@ -29,7 +30,7 @@ const General = () => { const { data: project } = useSelectedProjectQuery() const { data: organization } = useSelectedOrganizationQuery() - const { data: parentProject } = useProjectByRefQuery(project?.parent_project_ref) + const { data: parentProject } = useProjectDetailQuery({ ref: project?.parent_project_ref }) const isBranch = parentProject !== undefined const { projectSettingsRestartProject } = useIsFeatureEnabled([ diff --git a/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx b/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx index 2533fdfdf1df2..85b6e5d15694c 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx @@ -2,8 +2,9 @@ import Link from 'next/link' import SidePanelVercelProjectLinker from 'components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker' import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { useProjectByRefQuery, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { BASE_PATH } from 'lib/constants' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, WarningIcon } from 'ui' import GitHubSection from './GithubIntegration/GithubSection' @@ -21,7 +22,7 @@ export const IntegrationImageHandler = ({ title }: { title: 'vercel' | 'github' const IntegrationSettings = () => { const { data: project } = useSelectedProjectQuery() - const { data: parentProject } = useProjectByRefQuery(project?.parent_project_ref) + const { data: parentProject } = useProjectDetailQuery({ ref: project?.parent_project_ref }) const isBranch = project?.parent_project_ref !== undefined const showVercelIntegration = useIsFeatureEnabled('integrations:vercel') diff --git a/apps/studio/components/interfaces/Storage/EmptyBucketState.tsx b/apps/studio/components/interfaces/Storage/EmptyBucketState.tsx index 3b4e1a61a65e7..32a1b30ff017e 100644 --- a/apps/studio/components/interfaces/Storage/EmptyBucketState.tsx +++ b/apps/studio/components/interfaces/Storage/EmptyBucketState.tsx @@ -1,3 +1,4 @@ +import { BucketAdd } from 'icons' import { CreateBucketModal } from './CreateBucketModal' import { BUCKET_TYPES } from './Storage.constants' @@ -9,10 +10,13 @@ export const EmptyBucketState = ({ bucketType }: EmptyBucketStateProps) => { const config = BUCKET_TYPES[bucketType] return ( -
- - } - tooltip={{ content: { side: 'bottom', text: 'Clear messages' } }} - /> + {isContextExceededError ? ( + + ) : ( + <> + + } + tooltip={{ content: { side: 'bottom', text: 'Clear messages' } }} + /> + + )}
)} diff --git a/apps/studio/components/ui/Charts/ComposedChartHandler.tsx b/apps/studio/components/ui/Charts/ComposedChartHandler.tsx index e931c7ec848cb..008bb5acaded9 100644 --- a/apps/studio/components/ui/Charts/ComposedChartHandler.tsx +++ b/apps/studio/components/ui/Charts/ComposedChartHandler.tsx @@ -26,7 +26,7 @@ export interface ComposedChartHandlerProps { attributes: MultiAttribute[] startDate: string endDate: string - interval: string + interval?: string customDateFormat?: string defaultChartStyle?: 'bar' | 'line' | 'stackedAreaLine' hideChartType?: boolean diff --git a/apps/studio/components/ui/Charts/LogChartHandler.tsx b/apps/studio/components/ui/Charts/LogChartHandler.tsx deleted file mode 100644 index 6a8e4569def80..0000000000000 --- a/apps/studio/components/ui/Charts/LogChartHandler.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import { Loader2 } from 'lucide-react' -import React, { PropsWithChildren, useEffect, useRef, useState } from 'react' -import { cn, WarningIcon } from 'ui' - -import Panel from 'components/ui/Panel' -import { ComposedChart } from './ComposedChart' - -import { AnalyticsInterval } from 'data/analytics/constants' -import { useInfraMonitoringQueries } from 'data/analytics/infra-monitoring-queries' -import { InfraMonitoringAttribute } from 'data/analytics/infra-monitoring-query' -import { useProjectDailyStatsQueries } from 'data/analytics/project-daily-stats-queries' -import { ProjectDailyStatsAttribute } from 'data/analytics/project-daily-stats-query' -import { useChartHighlight } from './useChartHighlight' - -import type { UpdateDateRange } from 'pages/project/[ref]/reports/database' -import type { ChartData } from './Charts.types' -import type { MultiAttribute } from './ComposedChart.utils' - -interface LogChartHandlerProps { - id?: string - label: string - attributes: MultiAttribute[] - startDate: string - endDate: string - customDateFormat?: string - defaultChartStyle?: 'bar' | 'line' | 'stackedAreaLine' - hideChartType?: boolean - data?: ChartData - isLoading?: boolean - format?: string - highlightedValue?: string | number - className?: string - showTooltip?: boolean - showLegend?: boolean - showTotal?: boolean - showMaxValue?: boolean - updateDateRange: UpdateDateRange - valuePrecision?: number - isVisible?: boolean - titleTooltip?: string - docsUrl?: string - syncId?: string -} - -/** - * Wrapper component that handles intersection observer logic for lazy loading - */ -const LazyChartWrapper = ({ children }: PropsWithChildren) => { - const [isVisible, setIsVisible] = useState(false) - const ref = useRef(null) - - useEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting) { - setIsVisible(true) - observer.disconnect() - } - }, - { - rootMargin: '150px 0px', // Start loading before the component enters viewport - threshold: 0, - } - ) - - const currentRef = ref.current - if (currentRef) { - observer.observe(currentRef) - } - - return () => { - if (currentRef) { - observer.unobserve(currentRef) - } - } - }, []) - - return
{React.cloneElement(children as React.ReactElement, { isVisible })}
-} - -/** - * Controls chart display state. Optionally fetches static chart data if data is not provided. - * - * If the `data` prop is provided, it will disable automatic chart data fetching and pass the data directly to the chart render. - * - loading state can also be provided through the `isLoading` prop, to display loading placeholders. Ignored if `data` key not provided. - * - if `isLoading=true` and `data` is `undefined`, loading error message will be shown. - * - * Provided data must be in the expected chart format. - */ -export const LogChartHandler = ({ - label, - attributes, - customDateFormat, - children = null, - defaultChartStyle = 'bar', - hideChartType = false, - data, - isLoading, - format, - highlightedValue, - className, - showTooltip, - showLegend, - showMaxValue, - showTotal, - updateDateRange, - valuePrecision, - titleTooltip, - id, - syncId, - ...otherProps -}: PropsWithChildren) => { - const [chartStyle, setChartStyle] = useState(defaultChartStyle) - const chartHighlight = useChartHighlight() - - if (!data) { - return ( -
- -

Unable to load data for {label}

-
- ) - } - - // Rest of the component remains similar, but pass all attributes to charts - return ( - - {isLoading && ( -
- -

Loading data for {label}

-
- )} - -
{children}
- -
-
- ) -} - -export const useAttributeQueries = ( - attributes: MultiAttribute[], - ref: string | string[] | undefined, - startDate: string, - endDate: string, - interval: AnalyticsInterval, - databaseIdentifier: string | undefined, - data: ChartData | undefined, - isVisible: boolean -) => { - const infraAttributes = attributes.filter((attr) => attr.provider === 'infra-monitoring') - const dailyStatsAttributes = attributes.filter((attr) => attr.provider === 'daily-stats') - - const infraQueries = useInfraMonitoringQueries( - infraAttributes.map((attr) => attr.attribute as InfraMonitoringAttribute), - ref, - startDate, - endDate, - interval, - databaseIdentifier, - data, - isVisible - ) - const dailyStatsQueries = useProjectDailyStatsQueries( - dailyStatsAttributes.map((attr) => attr.attribute as ProjectDailyStatsAttribute), - ref, - startDate, - endDate, - data, - isVisible - ) - - let infraIdx = 0 - let dailyStatsIdx = 0 - return attributes - .filter((attr) => attr.provider !== 'logs') - .map((attr) => { - if (attr.provider === 'infra-monitoring') { - return { - ...infraQueries[infraIdx++], - data: { ...infraQueries[infraIdx - 1]?.data, provider: 'infra-monitoring' }, - } - } else if (attr.provider === 'daily-stats') { - return { - ...dailyStatsQueries[dailyStatsIdx++], - data: { ...dailyStatsQueries[dailyStatsIdx - 1]?.data, provider: 'daily-stats' }, - } - } else if (attr.provider === 'reference-line') { - let value = attr.value || 0 - return { - data: { - data: [], - attribute: attr.attribute, - total: value, - maximum: value, - totalGrouped: { [attr.attribute]: value }, - provider: 'reference-line', - }, - isLoading: false, - isError: false, - } - } else { - return { - isLoading: false, - data: undefined, - } - } - }) -} - -export default function LazyLogChartHandler(props: LogChartHandlerProps) { - return ( - - - - ) -} diff --git a/apps/studio/data/config/project-upgrade-eligibility-query.ts b/apps/studio/data/config/project-upgrade-eligibility-query.ts index 4515776f21002..90ddc5962c359 100644 --- a/apps/studio/data/config/project-upgrade-eligibility-query.ts +++ b/apps/studio/data/config/project-upgrade-eligibility-query.ts @@ -3,7 +3,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { components } from 'api-types' import { IS_PLATFORM } from 'common' import { get, handleError } from 'data/fetchers' -import { useProjectByRefQuery } from 'hooks/misc/useSelectedProject' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { PROJECT_STATUS } from 'lib/constants/infrastructure' import type { ResponseError } from 'types' import { configKeys } from './keys' @@ -38,7 +38,7 @@ export const useProjectUpgradeEligibilityQuery = = {} ) => { - const { data: project } = useProjectByRefQuery(projectRef) + const { data: project } = useProjectDetailQuery({ ref: projectRef }) return useQuery( configKeys.upgradeEligibility(projectRef), ({ signal }) => getProjectUpgradeEligibility({ projectRef }, signal), diff --git a/apps/studio/hooks/misc/useSelectedOrganization.ts b/apps/studio/hooks/misc/useSelectedOrganization.ts index 84e47836a985a..f7b1c4e7402da 100644 --- a/apps/studio/hooks/misc/useSelectedOrganization.ts +++ b/apps/studio/hooks/misc/useSelectedOrganization.ts @@ -1,12 +1,12 @@ import { useIsLoggedIn, useParams } from 'common' import { useOrganizationsQuery } from 'data/organizations/organizations-query' -import { useProjectByRefQuery } from './useSelectedProject' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' export function useSelectedOrganizationQuery({ enabled = true } = {}) { const isLoggedIn = useIsLoggedIn() const { ref, slug } = useParams() - const { data: selectedProject } = useProjectByRefQuery(ref) + const { data: selectedProject } = useProjectDetailQuery({ ref }) return useOrganizationsQuery({ enabled: isLoggedIn && enabled, diff --git a/apps/studio/hooks/misc/useSelectedProject.ts b/apps/studio/hooks/misc/useSelectedProject.ts index 6edab42d9bdc0..22ffd7df475b5 100644 --- a/apps/studio/hooks/misc/useSelectedProject.ts +++ b/apps/studio/hooks/misc/useSelectedProject.ts @@ -1,6 +1,5 @@ -import { useIsLoggedIn, useParams } from 'common' +import { useParams } from 'common' import { useProjectDetailQuery } from 'data/projects/project-detail-query' -import { useProjectsQuery } from 'data/projects/projects-query' import { PROVIDERS } from 'lib/constants' export function useSelectedProjectQuery({ enabled = true } = {}) { @@ -17,28 +16,6 @@ export function useSelectedProjectQuery({ enabled = true } = {}) { ) } -export function useProjectByRefQuery(ref?: string) { - const isLoggedIn = useIsLoggedIn() - - const projectQuery = useProjectDetailQuery({ ref }, { enabled: isLoggedIn }) - - // [Alaister]: This is here for the purpose of improving performance. - // Chances are, the user will already have the list of projects in the cache. - // We can't exclusively rely on this method, as useProjectsQuery does not return branch projects. - const projectsQuery = useProjectsQuery({ - enabled: isLoggedIn, - select: (data) => { - return data.projects.find((project) => project.ref === ref) - }, - }) - - if (projectQuery.isSuccess) { - return projectQuery - } - - return projectsQuery -} - export const useIsAwsCloudProvider = () => { const { data: project } = useSelectedProjectQuery() const isAws = project?.cloud_provider === PROVIDERS.AWS.id diff --git a/apps/studio/lib/api/apiAuthenticate.test.ts b/apps/studio/lib/api/apiAuthenticate.test.ts index e11fac9a09cc4..5ca4667664023 100644 --- a/apps/studio/lib/api/apiAuthenticate.test.ts +++ b/apps/studio/lib/api/apiAuthenticate.test.ts @@ -3,9 +3,9 @@ import { apiAuthenticate } from './apiAuthenticate' const mocks = vi.hoisted(() => { return { - getAuthUser: vi.fn().mockResolvedValue({ - user: { - id: 'test-gotrue-id', + getUserClaims: vi.fn().mockResolvedValue({ + claims: { + sub: 'test-gotrue-id', email: 'test@example.com', }, error: null, @@ -14,7 +14,7 @@ const mocks = vi.hoisted(() => { }) vi.mock('lib/gotrue', () => ({ - getAuthUser: mocks.getAuthUser, + getUserClaims: mocks.getUserClaims, })) describe('apiAuthenticate', () => { @@ -29,9 +29,9 @@ describe('apiAuthenticate', () => { beforeEach(() => { vi.clearAllMocks() - mocks.getAuthUser.mockResolvedValue({ - user: { - id: 'test-gotrue-id', + mocks.getUserClaims.mockResolvedValue({ + claims: { + sub: 'test-gotrue-id', email: 'test@example.com', }, error: null, @@ -45,8 +45,8 @@ describe('apiAuthenticate', () => { }) it('should return error when auth user fetch fails', async () => { - mocks.getAuthUser.mockResolvedValue({ - user: null, + mocks.getUserClaims.mockResolvedValue({ + claims: null, error: new Error('Auth failed'), }) @@ -55,8 +55,8 @@ describe('apiAuthenticate', () => { }) it('should return error when user does not exist', async () => { - mocks.getAuthUser.mockResolvedValue({ - user: null, + mocks.getUserClaims.mockResolvedValue({ + claims: null, error: null, }) diff --git a/apps/studio/lib/api/apiAuthenticate.ts b/apps/studio/lib/api/apiAuthenticate.ts index bcca2e361f400..02922dfd80ce2 100644 --- a/apps/studio/lib/api/apiAuthenticate.ts +++ b/apps/studio/lib/api/apiAuthenticate.ts @@ -1,6 +1,7 @@ -import { getAuthUser } from 'lib/gotrue' +import type { JwtPayload } from '@supabase/supabase-js' +import { getUserClaims } from 'lib/gotrue' import type { NextApiRequest, NextApiResponse } from 'next' -import type { ResponseError, SupaResponse, User } from 'types' +import type { ResponseError } from 'types' /** * Use this method on api routes to check if user is authenticated and having required permissions. @@ -15,14 +16,14 @@ import type { ResponseError, SupaResponse, User } from 'types' export async function apiAuthenticate( req: NextApiRequest, _res: NextApiResponse -): Promise> { +): Promise { try { - const user = await fetchUser(req) - if (!user) { + const claims = await fetchUserClaims(req) + if (!claims) { return { error: new Error('The user does not exist') } } - return user + return claims } catch (error) { return { error: error as ResponseError } } @@ -32,19 +33,19 @@ export async function apiAuthenticate( * @returns * user with only id prop or detail object. It depends on requireUserDetail config */ -async function fetchUser(req: NextApiRequest): Promise { - const token = req.headers.authorization?.replace('Bearer ', '') +async function fetchUserClaims(req: NextApiRequest): Promise { + const token = req.headers.authorization?.replace(/bearer /i, '') if (!token) { throw new Error('missing access token') } - const { user, error } = await getAuthUser(token) + const { claims, error } = await getUserClaims(token) if (error) { throw error } - if (!user) { + if (!claims) { throw new Error('The user does not exist') } - return user + return claims } diff --git a/apps/studio/lib/api/apiWrapper.ts b/apps/studio/lib/api/apiWrapper.ts index fb9d8bea32704..901c5a5501b64 100644 --- a/apps/studio/lib/api/apiWrapper.ts +++ b/apps/studio/lib/api/apiWrapper.ts @@ -40,9 +40,6 @@ export default async function apiWrapper( message: `Unauthorized: ${response.error.message}`, }, }) - } else { - // Attach user information to request parameters - ;(req as any).user = response } } diff --git a/apps/studio/lib/api/apiWrappers.test.ts b/apps/studio/lib/api/apiWrappers.test.ts index 0768780ef28ca..9fc8d326220dc 100644 --- a/apps/studio/lib/api/apiWrappers.test.ts +++ b/apps/studio/lib/api/apiWrappers.test.ts @@ -28,14 +28,4 @@ describe('apiWrapper', () => { expect(mockHandler).toHaveBeenCalledWith(mockReq, mockRes) expect(apiAuthenticate).not.toHaveBeenCalled() }) - - it('should attach user to request and call handler when authentication succeeds', async () => { - const mockUser = { id: '123', email: 'test@example.com' } as any as any - vi.mocked(apiAuthenticate).mockResolvedValue(mockUser) - - await apiWrapper(mockReq, mockRes, mockHandler, { withAuth: true }) - - expect(mockReq.user).toEqual(mockUser) - expect(mockHandler).toHaveBeenCalledWith(mockReq, mockRes) - }) }) diff --git a/apps/studio/lib/gotrue.ts b/apps/studio/lib/gotrue.ts index 42c65673ae69a..b51fdd29e7111 100644 --- a/apps/studio/lib/gotrue.ts +++ b/apps/studio/lib/gotrue.ts @@ -1,3 +1,4 @@ +import type { JwtPayload } from '@supabase/supabase-js' import { getAccessToken, type User } from 'common/auth' import { gotrueClient } from 'common/gotrue' @@ -23,18 +24,17 @@ export const validateReturnTo = ( return safePathPattern.test(returnTo) ? returnTo : fallback } -export const getAuthUser = async (token: String): Promise => { +export const getUserClaims = async ( + token: String +): Promise<{ error: any | null; claims: JwtPayload | null }> => { try { - const { - data: { user }, - error, - } = await auth.getUser(token.replace('Bearer ', '')) + const { data, error } = await auth.getClaims(token.replace(/bearer /i, '')) if (error) throw error - return { user, error: null } + return { claims: data?.claims ?? null, error: null } } catch (err) { console.error(err) - return { user: null, error: err } + return { claims: null, error: err } } } diff --git a/apps/studio/next-env.d.ts b/apps/studio/next-env.d.ts new file mode 100644 index 0000000000000..254b73c165d90 --- /dev/null +++ b/apps/studio/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/apps/studio/pages/project/[ref]/merge.tsx b/apps/studio/pages/project/[ref]/merge.tsx index 21e946bf6d845..5e3a9751f07a5 100644 --- a/apps/studio/pages/project/[ref]/merge.tsx +++ b/apps/studio/pages/project/[ref]/merge.tsx @@ -22,11 +22,12 @@ import { useBranchMergeMutation } from 'data/branches/branch-merge-mutation' import { useBranchPushMutation } from 'data/branches/branch-push-mutation' import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation' import { useBranchesQuery } from 'data/branches/branches-query' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useBranchMergeDiff } from 'hooks/branches/useBranchMergeDiff' import { useWorkflowManagement } from 'hooks/branches/useWorkflowManagement' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' -import { useProjectByRefQuery, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import type { NextPageWithLayout } from 'types' import { Badge, @@ -54,7 +55,7 @@ const MergePage: NextPageWithLayout = () => { const isBranch = project?.parent_project_ref !== undefined const parentProjectRef = project?.parent_project_ref - const { data: parentProject } = useProjectByRefQuery(parentProjectRef) + const { data: parentProject } = useProjectDetailQuery({ ref: parentProjectRef }) const { data: branches } = useBranchesQuery( { projectRef: parentProjectRef }, diff --git a/packages/build-icons/src/building/generateIconFiles.mjs b/packages/build-icons/src/building/generateIconFiles.mjs index baaebedd2a68c..1652a2ac022e3 100644 --- a/packages/build-icons/src/building/generateIconFiles.mjs +++ b/packages/build-icons/src/building/generateIconFiles.mjs @@ -52,18 +52,19 @@ export const Index: Record = [` let { children } = iconNodes[iconName] children = children.map(({ name, attributes }) => [name, attributes]) - const getSvg = () => readSvg(`${iconName}.svg`, iconsDir) + const svgContent = readSvg(`${iconName}.svg`, iconsDir) + const getSvg = () => svgContent // const { deprecated = false } = iconMetaData[iconName] const deprecated = false const elementTemplate = template({ componentName, iconName, children, getSvg, deprecated }) const output = pretty - ? prettier.format(elementTemplate, { - singleQuote: true, - trailingComma: 'all', - printWidth: 100, - parser: 'babel', - }) + ? await prettier.format(elementTemplate, { + singleQuote: true, + trailingComma: 'all', + printWidth: 100, + parser: 'babel', + }) : elementTemplate const rawSvg = JSON.stringify(readSvg(`${iconName}.svg`, iconsDir)) @@ -111,7 +112,7 @@ export const Index: Record = [` } // TO DO -- END - Promise.all([writeIconFiles]) + Promise.all(writeIconFiles) // TO DO -- START // diff --git a/packages/icons/README.md b/packages/icons/README.md index 2e6956831c331..80bb01cf2ff24 100644 --- a/packages/icons/README.md +++ b/packages/icons/README.md @@ -1,122 +1,31 @@ # ./packages/icons -This package is for custom Supabase icons -They can be used alongside any other icon packages +This package contains custom Supabase icons that can be used alongside other icon libraries. -## example use +## Documentation + +**For complete documentation, usage examples, and guidelines, see the [Design System](../../apps/design-system/content/docs/icons.mdx)** + +## Quick start ```jsx -import { ReplaceCode, InsertCode } from 'icons' +import { BucketAdd, Database, Auth } from 'icons' -function app() { +function MyComponent() { return ( <> - - + + + ) } ``` -## adding new icons - -Add new icons into ./src/raw-icons - -Make sure there are no inline stroke/border/fill colors (see below) - -run this in ./packages/build-icons - -```bash -npm run build -``` - -This will output icons into ./src/icons and update import names/paths - -### Design spec - -Icons should: - -- always be exported 24x24px, -- and have an icon inside that frame that's around 18x18px(ish) - -### ❌ bad example - -Notice the stroke, stroke-linecap, fills, etc. -These need to be in the parent so the react component can easily control it. -The SVG child elements will then respect their parent's attributes. - -```svg - - - - - - - - - -``` - -✅ Good example +### Adding new custom icons -We've now cleaned it up, and the parent SVG element now has all the attributes for color and stroke width styling. -We have also removed the redundant elements that figma adds in like background / artboard backgrounds. +1. Add your SVG file to `src/raw-icons/` (kebab-case name) +2. Run `npm run build:icons` in this directory +3. Import and use your new icon -```svg - - - - - - - -``` +For detailed instructions, examples, and troubleshooting, see the [Design System](../../apps/design-system/content/docs/icons.mdx). diff --git a/packages/icons/__registry__/index.tsx b/packages/icons/__registry__/index.tsx index 3d22f40f027e0..eaac6c2fa1e76 100644 --- a/packages/icons/__registry__/index.tsx +++ b/packages/icons/__registry__/index.tsx @@ -44,6 +44,26 @@ export const Index: Record = [ svg: "\n \n", jsx: "import { Auth } from \"icons\"\n \n " }, +{ + name: "bucket-add", + componentName: "BucketAdd", + deprecated: false, + raw: "import createSupabaseIcon from '../createSupabaseIcon';\n\n/**\n * @component @name BucketAdd\n * @description Supabase SVG icon component, renders SVG Element with children.\n *\n * @preview ![img](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNiA3QzYgNC4yIDguMiAyIDExIDJIMTNDMTUuOCAyIDE4IDQuMiAxOCA3IiBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTQuNSAxMUgxOS41IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNNiAxMUw2LjggMjBDNi45IDIxLjEgNy45IDIyIDkgMjJIMTIiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xOCAxNS4wMjQ5VjIyLjAyNDkiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xNC41IDE4LjUyNDlIMjEuNSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==)\n *\n * @param {Object} props - Supabase icons props and any valid SVG attribute\n * @returns {JSX.Element} JSX Element\n *\n */\nconst BucketAdd = createSupabaseIcon('BucketAdd', [\n [\n 'path',\n {\n d: 'M6 7C6 4.2 8.2 2 11 2H13C15.8 2 18 4.2 18 7',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: '9nyc2k',\n },\n ],\n [\n 'path',\n {\n d: 'M4.5 11H19.5',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: 'fq7r7q',\n },\n ],\n [\n 'path',\n {\n d: 'M6 11L6.8 20C6.9 21.1 7.9 22 9 22H12',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: 'f9mz1i',\n },\n ],\n [\n 'path',\n {\n d: 'M18 15.0249V22.0249',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: '7kgnjd',\n },\n ],\n [\n 'path',\n {\n d: 'M14.5 18.5249H21.5',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: '4i9ntd',\n },\n ],\n]);\n\nexport default BucketAdd;\n", + component: React.lazy(() => import('icons/src/icons/bucket-add')), + import: "import { BucketAdd } from 'icons'", + svg: "\n\n\n\n\n\n\n", + jsx: "import { BucketAdd } from \"icons\"\n \n " +}, +{ + name: "bucket", + componentName: "Bucket", + deprecated: false, + raw: "import createSupabaseIcon from '../createSupabaseIcon';\n\n/**\n * @component @name Bucket\n * @description Supabase SVG icon component, renders SVG Element with children.\n *\n * @preview ![img](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNiA3QzYgNC4yIDguMiAyIDExIDJIMTNDMTUuOCAyIDE4IDQuMiAxOCA3IiBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTQuNSAxMUgxOS41IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMTggMTFMMTcuMiAyMEMxNy4xIDIxLjEgMTYuMSAyMiAxNSAyMkg5QzcuOSAyMiA2LjkgMjEuMSA2LjggMjBMNiAxMSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==)\n *\n * @param {Object} props - Supabase icons props and any valid SVG attribute\n * @returns {JSX.Element} JSX Element\n *\n */\nconst Bucket = createSupabaseIcon('Bucket', [\n [\n 'path',\n {\n d: 'M6 7C6 4.2 8.2 2 11 2H13C15.8 2 18 4.2 18 7',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: '9nyc2k',\n },\n ],\n [\n 'path',\n {\n d: 'M4.5 11H19.5',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: 'fq7r7q',\n },\n ],\n [\n 'path',\n {\n d: 'M18 11L17.2 20C17.1 21.1 16.1 22 15 22H9C7.9 22 6.9 21.1 6.8 20L6 11',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n key: 'nfdgw2',\n },\n ],\n]);\n\nexport default Bucket;\n", + component: React.lazy(() => import('icons/src/icons/bucket')), + import: "import { Bucket } from 'icons'", + svg: "\n\n\n\n\n", + jsx: "import { Bucket } from \"icons\"\n \n " +}, { name: "database", componentName: "Database", diff --git a/packages/icons/src/createSupabaseIcon.ts b/packages/icons/src/createSupabaseIcon.ts index 5671d38e55255..ae99cc600f55f 100644 --- a/packages/icons/src/createSupabaseIcon.ts +++ b/packages/icons/src/createSupabaseIcon.ts @@ -33,6 +33,33 @@ export const toKebabCase = (string: string) => .toLowerCase() .trim() +/** + * Converts kebab-case string to camelCase + * @param {string} string + * @returns {string} A camelCased string + */ +export const toCamelCase = (string: string) => + string.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).trim() + +/** + * Converts kebab-case attributes to camelCase for React compatibility + * @param {Record} attrs + * @returns {Record} Attributes with camelCase keys + */ +export const convertAttributesToCamelCase = ( + attrs: Record +): Record => { + const converted: Record = {} + + for (const [key, value] of Object.entries(attrs)) { + // Convert kebab-case to camelCase, but keep some special cases + const camelKey = key.includes('-') ? toCamelCase(key) : key + converted[camelKey] = value + } + + return converted +} + const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => { const Component = forwardRef( ( @@ -62,7 +89,9 @@ const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => { ...rest, }, [ - ...iconNode.map(([tag, attrs]) => createElement(tag, attrs)), + ...iconNode.map(([tag, attrs]) => + createElement(tag, convertAttributesToCamelCase(attrs)) + ), ...(Array.isArray(children) ? children : [children]), ] ) diff --git a/packages/icons/src/icons/bucket-add.ts b/packages/icons/src/icons/bucket-add.ts new file mode 100644 index 0000000000000..c3021e5e2c770 --- /dev/null +++ b/packages/icons/src/icons/bucket-add.ts @@ -0,0 +1,71 @@ +import createSupabaseIcon from '../createSupabaseIcon'; + +/** + * @component @name BucketAdd + * @description Supabase SVG icon component, renders SVG Element with children. + * + * @preview ![img](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNiA3QzYgNC4yIDguMiAyIDExIDJIMTNDMTUuOCAyIDE4IDQuMiAxOCA3IiBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTQuNSAxMUgxOS41IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNNiAxMUw2LjggMjBDNi45IDIxLjEgNy45IDIyIDkgMjJIMTIiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xOCAxNS4wMjQ5VjIyLjAyNDkiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xNC41IDE4LjUyNDlIMjEuNSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==) + * + * @param {Object} props - Supabase icons props and any valid SVG attribute + * @returns {JSX.Element} JSX Element + * + */ +const BucketAdd = createSupabaseIcon('BucketAdd', [ + [ + 'path', + { + d: 'M6 7C6 4.2 8.2 2 11 2H13C15.8 2 18 4.2 18 7', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: '9nyc2k', + }, + ], + [ + 'path', + { + d: 'M4.5 11H19.5', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: 'fq7r7q', + }, + ], + [ + 'path', + { + d: 'M6 11L6.8 20C6.9 21.1 7.9 22 9 22H12', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: 'f9mz1i', + }, + ], + [ + 'path', + { + d: 'M18 15.0249V22.0249', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: '7kgnjd', + }, + ], + [ + 'path', + { + d: 'M14.5 18.5249H21.5', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: '4i9ntd', + }, + ], +]); + +export default BucketAdd; diff --git a/packages/icons/src/icons/bucket.ts b/packages/icons/src/icons/bucket.ts new file mode 100644 index 0000000000000..869e4a6bce67d --- /dev/null +++ b/packages/icons/src/icons/bucket.ts @@ -0,0 +1,49 @@ +import createSupabaseIcon from '../createSupabaseIcon'; + +/** + * @component @name Bucket + * @description Supabase SVG icon component, renders SVG Element with children. + * + * @preview ![img](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNiA3QzYgNC4yIDguMiAyIDExIDJIMTNDMTUuOCAyIDE4IDQuMiAxOCA3IiBzdHJva2U9IiMwMDAiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmOyBib3JkZXItcmFkaXVzOiAycHgiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTQuNSAxMUgxOS41IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMTggMTFMMTcuMiAyMEMxNy4xIDIxLjEgMTYuMSAyMiAxNSAyMkg5QzcuOSAyMiA2LjkgMjEuMSA2LjggMjBMNiAxMSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==) + * + * @param {Object} props - Supabase icons props and any valid SVG attribute + * @returns {JSX.Element} JSX Element + * + */ +const Bucket = createSupabaseIcon('Bucket', [ + [ + 'path', + { + d: 'M6 7C6 4.2 8.2 2 11 2H13C15.8 2 18 4.2 18 7', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: '9nyc2k', + }, + ], + [ + 'path', + { + d: 'M4.5 11H19.5', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: 'fq7r7q', + }, + ], + [ + 'path', + { + d: 'M18 11L17.2 20C17.1 21.1 16.1 22 15 22H9C7.9 22 6.9 21.1 6.8 20L6 11', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + key: 'nfdgw2', + }, + ], +]); + +export default Bucket; diff --git a/packages/icons/src/icons/index.ts b/packages/icons/src/icons/index.ts index 4eab2e678fa24..9dc947c868175 100644 --- a/packages/icons/src/icons/index.ts +++ b/packages/icons/src/icons/index.ts @@ -2,6 +2,8 @@ export { default as RESTApi } from './REST-api'; export { default as ExampleTemplate } from './_example-template'; export { default as ApiDocs } from './api-docs'; export { default as Auth } from './auth'; +export { default as BucketAdd } from './bucket-add'; +export { default as Bucket } from './bucket'; export { default as Database } from './database'; export { default as Datadog } from './datadog'; export { default as EdgeFunctions } from './edge-functions'; diff --git a/packages/icons/src/raw-icons/_example-template.json b/packages/icons/src/raw-icons/_example-template.json deleted file mode 100644 index 30e98ddf1555e..0000000000000 --- a/packages/icons/src/raw-icons/_example-template.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "../icon.schema.json", - "contributors": ["it-is-not", "jguddas", "danielbayley", "ericfennis"], - "tags": ["letter", "font size", "text", "formatting", "smaller"], - "categories": ["text", "design"] -} diff --git a/packages/icons/src/raw-icons/bucket-add.svg b/packages/icons/src/raw-icons/bucket-add.svg new file mode 100644 index 0000000000000..57b907bbc77ea --- /dev/null +++ b/packages/icons/src/raw-icons/bucket-add.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/icons/src/raw-icons/bucket.svg b/packages/icons/src/raw-icons/bucket.svg new file mode 100644 index 0000000000000..f03ab000b21c0 --- /dev/null +++ b/packages/icons/src/raw-icons/bucket.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/shared-data/regions.ts b/packages/shared-data/regions.ts index 51d59e0d9105b..4eb43a4545b90 100644 --- a/packages/shared-data/regions.ts +++ b/packages/shared-data/regions.ts @@ -77,3 +77,9 @@ export type AWS_REGIONS_KEYS = keyof typeof AWS_REGIONS export const FLY_REGIONS = { SOUTHEAST_ASIA: { code: 'sin', displayName: 'Singapore', location: [1.3521, 103.8198] }, } as const + +export const SMART_REGION_TO_EXACT_REGION_MAP = new Map([ + ['Americas', 'East US (North Virginia)'], + ['Europe', 'Central EU (Frankfurt)'], + ['APAC', 'Southeast Asia (Singapore)'], +]) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8953bc8888bb1..691c7ece6bd70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -314,6 +314,9 @@ importers: '@types/react-dom': specifier: 'catalog:' version: 18.3.0 + concurrently: + specifier: ^8.2.2 + version: 8.2.2 config: specifier: workspace:^ version: link:../../packages/config @@ -11036,6 +11039,11 @@ packages: concat-with-sourcemaps@1.1.0: resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} + concurrently@8.2.2: + resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} + engines: {node: ^14.13.0 || >=16.0.0} + hasBin: true + concurrently@9.1.2: resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==} engines: {node: '>=18'} @@ -18045,6 +18053,9 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -31831,6 +31842,18 @@ snapshots: dependencies: source-map: 0.6.1 + concurrently@8.2.2: + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.21 + rxjs: 7.8.1 + shell-quote: 1.8.3 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + concurrently@9.1.2: dependencies: chalk: 4.1.2 @@ -40720,6 +40743,8 @@ snapshots: space-separated-tokens@2.0.2: {} + spawn-command@0.0.2: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1