diff --git a/.github/workflows/studio-e2e-test.yml b/.github/workflows/studio-e2e-test.yml index 42eb11cf90642..dccb143223758 100644 --- a/.github/workflows/studio-e2e-test.yml +++ b/.github/workflows/studio-e2e-test.yml @@ -28,8 +28,8 @@ jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest - # Require approval only for external contributors - environment: ${{ github.event.pull_request.author_association != 'MEMBER' && 'Studio E2E Tests' || '' }} + # Require approval only for pull requests from forks + environment: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork && 'Studio E2E Tests' || '' }} env: EMAIL: ${{ secrets.CI_EMAIL }} diff --git a/apps/studio/components/interfaces/Auth/Overview/OverviewUsage.tsx b/apps/studio/components/interfaces/Auth/Overview/OverviewUsage.tsx index 0ba0062ea149c..2da5ec03eaf7b 100644 --- a/apps/studio/components/interfaces/Auth/Overview/OverviewUsage.tsx +++ b/apps/studio/components/interfaces/Auth/Overview/OverviewUsage.tsx @@ -59,7 +59,11 @@ export const StatCard = ({ : 'text-brand' : getChangeColor(previous) const formattedCurrent = - suffix === 'ms' ? current.toFixed(2) : suffix === '%' ? current.toFixed(1) : Math.round(current) + suffix === 'ms' + ? current.toFixed(2) + : suffix === '%' + ? current.toFixed(1) + : Math.round(current).toLocaleString() const signChar = previous > 0 ? '+' : previous < 0 ? '-' : '' return ( diff --git a/apps/studio/components/interfaces/Organization/HeaderBanner.tsx b/apps/studio/components/interfaces/Organization/HeaderBanner.tsx index 0754cc2c2c5c1..9da5f4e65bda4 100644 --- a/apps/studio/components/interfaces/Organization/HeaderBanner.tsx +++ b/apps/studio/components/interfaces/Organization/HeaderBanner.tsx @@ -50,6 +50,7 @@ export const HeaderBanner = ({ type === 'incident' && 'hover:bg-brand-300', 'flex-shrink-0' )} + layout="position" >
@@ -98,15 +99,16 @@ export const HeaderBanner = ({
{link && ( - + View Details + )}
diff --git a/apps/studio/components/interfaces/Storage/BucketRow.tsx b/apps/studio/components/interfaces/Storage/BucketRow.tsx index 4cfcdeb3794ab..8024003444f46 100644 --- a/apps/studio/components/interfaces/Storage/BucketRow.tsx +++ b/apps/studio/components/interfaces/Storage/BucketRow.tsx @@ -1,6 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { Columns3, Edit2, MoreVertical, Trash, XCircle } from 'lucide-react' import Link from 'next/link' +import type { CSSProperties } from 'react' import { useState } from 'react' import { DeleteBucketModal } from 'components/interfaces/Storage/DeleteBucketModal' @@ -26,9 +27,17 @@ export interface BucketRowProps { bucket: Bucket projectRef?: string isSelected: boolean + style?: CSSProperties + className?: string } -export const BucketRow = ({ bucket, projectRef = '', isSelected = false }: BucketRowProps) => { +export const BucketRow = ({ + bucket, + projectRef = '', + isSelected = false, + style, + className, +}: BucketRowProps) => { const { can: canUpdateBuckets } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*') const [modal, setModal] = useState(null) const onClose = () => setModal(null) @@ -36,9 +45,11 @@ export const BucketRow = ({ bucket, projectRef = '', isSelected = false }: Bucke return (
{/* Even though we trim whitespaces from bucket names, there may be some existing buckets with trailing whitespaces. */} diff --git a/apps/studio/components/interfaces/Storage/StorageMenu.BucketList.tsx b/apps/studio/components/interfaces/Storage/StorageMenu.BucketList.tsx new file mode 100644 index 0000000000000..b49c691a71f6b --- /dev/null +++ b/apps/studio/components/interfaces/Storage/StorageMenu.BucketList.tsx @@ -0,0 +1,120 @@ +import type { CSSProperties } from 'react' +import { memo, useLayoutEffect, useMemo, useRef, useState } from 'react' +import type { ListChildComponentProps } from 'react-window' +import { FixedSizeList as List, areEqual } from 'react-window' + +import type { Bucket } from 'data/storage/buckets-query' +import { BucketRow } from './BucketRow' + +type BucketListProps = { + buckets: Bucket[] + selectedBucketId?: string + projectRef?: string +} + +const BUCKET_ROW_HEIGHT = 'h-7' + +const VirtualizedBucketRow = memo( + ({ index, style, data }: ListChildComponentProps) => { + const bucket = data.buckets[index] + const isSelected = data.selectedBucketId === bucket.id + + return ( + + ) + }, + (prev, next) => { + if (!areEqual(prev, next)) return false + + const prevBucket = prev.data.buckets[prev.index] + const nextBucket = next.data.buckets[next.index] + + if (prevBucket !== nextBucket) return false + + const wasSelected = prev.data.selectedBucketId === prevBucket.id + const isSelected = next.data.selectedBucketId === nextBucket.id + + return wasSelected === isSelected + } +) +VirtualizedBucketRow.displayName = 'VirtualizedBucketRow' + +const BucketListVirtualized = ({ buckets, selectedBucketId, projectRef = '' }: BucketListProps) => { + const [listHeight, setListHeight] = useState(500) + const sizerRef = useRef(null) + + useLayoutEffect(() => { + if (sizerRef.current) { + const resizeObserver = new ResizeObserver(([entry]) => { + const { height } = entry.contentRect + setListHeight(height) + }) + + resizeObserver.observe(sizerRef.current) + setListHeight(sizerRef.current.getBoundingClientRect().height) + + return () => { + resizeObserver.disconnect() + } + } + }, []) + + const itemData = useMemo( + () => ({ + buckets, + projectRef, + selectedBucketId, + }), + [buckets, projectRef, selectedBucketId] + ) + + return ( +
+ buckets[index].id} + height={listHeight} + // itemSize should match the height of BucketRow + any gap/margin + itemSize={28} + width="100%" + > + {VirtualizedBucketRow} + +
+ ) +} + +export const BucketList = ({ buckets, selectedBucketId, projectRef = '' }: BucketListProps) => { + const numBuckets = buckets.length + + if (numBuckets <= 50) { + return ( +
+ {buckets.map((bucket) => ( + + ))} +
+ ) + } + + return ( + + ) +} diff --git a/apps/studio/components/interfaces/Storage/StorageMenu.tsx b/apps/studio/components/interfaces/Storage/StorageMenu.tsx index a0c3243806ed7..8ffd4ca64e6c8 100644 --- a/apps/studio/components/interfaces/Storage/StorageMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageMenu.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' import { useRouter } from 'next/router' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { useFlag, useParams } from 'common' import { CreateBucketModal } from 'components/interfaces/Storage/CreateBucketModal' @@ -19,7 +19,7 @@ import { InnerSideBarFilterSortDropdown, InnerSideBarFilterSortDropdownItem, } from 'ui-patterns/InnerSideMenu' -import { BucketRow } from './BucketRow' +import { BucketList } from './StorageMenu.BucketList' export const StorageMenu = () => { const router = useRouter() @@ -53,21 +53,31 @@ export const StorageMenu = () => { isError, isSuccess, } = useBucketsQuery({ projectRef: ref }) - const sortedBuckets = - snap.sortBucket === 'alphabetical' - ? buckets.sort((a, b) => - a.name.toLowerCase().trim().localeCompare(b.name.toLowerCase().trim()) - ) - : buckets.sort((a, b) => (new Date(b.created_at) > new Date(a.created_at) ? -1 : 1)) - const filteredBuckets = - searchText.length > 1 - ? sortedBuckets.filter((bucket) => bucket.name.includes(searchText.trim())) - : sortedBuckets + const sortedBuckets = useMemo( + () => + snap.sortBucket === 'alphabetical' + ? buckets.sort((a, b) => + a.name.toLowerCase().trim().localeCompare(b.name.toLowerCase().trim()) + ) + : buckets.sort((a, b) => (new Date(b.created_at) > new Date(a.created_at) ? -1 : 1)), + [buckets, snap.sortBucket] + ) + const filteredBuckets = useMemo( + () => + searchText.length > 1 + ? sortedBuckets.filter((bucket) => bucket.name.includes(searchText.trim())) + : sortedBuckets, + [sortedBuckets, searchText] + ) const tempNotSupported = error?.message.includes('Tenant config') && isBranch return ( <> - +
@@ -100,8 +110,8 @@ export const StorageMenu = () => {
-
-
+
+
All buckets} /> {isLoading && ( @@ -145,17 +155,13 @@ export const StorageMenu = () => { description={`Your search for "${searchText}" did not return any results`} /> )} - {filteredBuckets.map((bucket, idx: number) => { - const isSelected = bucketId === bucket.id - return ( - - ) - })} + {filteredBuckets.length > 0 && ( + + )} )}
diff --git a/e2e/studio/.env.local.example b/e2e/studio/.env.local.example index 256a6101fbd45..400bfc19a613b 100644 --- a/e2e/studio/.env.local.example +++ b/e2e/studio/.env.local.example @@ -1,14 +1,9 @@ -# 1. Copy and paste this file and rename it to .env.local +# Copy and paste this file and rename it to .env.local -# 2. Set the STUDIO_URL and API_URL you want the e2e tests to run against +STUDIO_URL=http://127.0.0.1:54323 +API_URL=http://127.0.0.1:54323 +IS_PLATFORM=false -STUDIO_URL=https://supabase.com/dashboard -API_URL=https://api.supabase.com -AUTHENTICATION=true - -# 3. *Optional* If the environment requires auth, set AUTHENTICATION to true, auth credentials, and PROJECT_REF - -EMAIL= -PASSWORD= -PROJECT_REF= +# Used to run e2e tests against vercel previews +VERCEL_AUTOMATION_BYPASS_SELFHOSTED_STUDIO= \ No newline at end of file diff --git a/e2e/studio/README.md b/e2e/studio/README.md index 6ddfe554a10bc..b3d031623e458 100644 --- a/e2e/studio/README.md +++ b/e2e/studio/README.md @@ -6,60 +6,24 @@ cp .env.local.example .env.local ``` -Edit the `.env.local` file with your credentials and environment. - ### Install the playwright browser ⚠️ This should be done in the `e2e/studio` directory ```bash -pnpm exec playwright install -``` - -## Environments - -### Staging - -```bash -STUDIO_URL=https://supabase.green/dashboard -API_URL=https://api.supabase.green -AUTHENTICATION=true -EMAIL=your@email.com -PASSWORD=yourpassword -PROJECT_REF=yourprojectref -``` - -### CLI (NO AUTH) +cd e2e/studio -You'll need to run the CLI locally. - -```bash -STUDIO_URL=http://localhost:54323 -API_URL=http://localhost:54323/api -AUTHENTICATION=false +pnpm exec playwright install ``` -### CLI Development (NO AUTH) +### Run a local Supabase instance -You'll need to run Studio in development mode with `IS_PLATFORM=false` +Make sure you have Supabase CLI installed ```bash -STUDIO_URL=http://localhost:8082/ -API_URL=http://localhost:8082/api -AUTHENTICATION=false -``` - -### Hosted Development - -You'll need to run Studio in development mode with `IS_PLATFORM=true` +cd e2e/studio -```bash -STUDIO_URL=http://localhost:8082/ -API_URL=http://localhost:8080/api -AUTHENTICATION=true -EMAIL=your@email.com -PASSWORD=yourpassword -PROJECT_REF=yourprojectref +supabase start ``` --- @@ -68,8 +32,6 @@ PROJECT_REF=yourprojectref Check the `package.json` for the available commands and environments. -#### Example: - ```bash pnpm run e2e ``` @@ -111,19 +73,6 @@ PWDEBUG=1 pnpm run e2e -- --ui --- -## Organization - -Name the folders based on the feature you are testing. - -```bash -e2e/studio/logs/ -e2e/studio/sql-editor/ -e2e/studio/storage/ -e2e/studio/auth/ -``` - ---- - ## What should I test? - Can the feature be navigated to?