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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/studio-e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const HeaderBanner = ({
type === 'incident' && 'hover:bg-brand-300',
'flex-shrink-0'
)}
layout="position"
>
<div className={cn('items-center flex flex-row gap-3')}>
<div className="absolute inset-y-0 left-0 right-0 overflow-hidden z-0">
Expand Down Expand Up @@ -98,15 +99,16 @@ export const HeaderBanner = ({
</span>
</div>
{link && (
<button
<a
href={link}
className={cn(
'lg:block hidden',
'text-foreground-lighter text-sm z-[1] m-0',
type === 'danger' ? 'text-destructive' : 'text-warning'
)}
>
<Link href={link}>View Details</Link>
</button>
View Details
</a>
)}
</div>
</motion.div>
Expand Down
15 changes: 13 additions & 2 deletions apps/studio/components/interfaces/Storage/BucketRow.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -26,19 +27,29 @@ 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<string | null>(null)
const onClose = () => setModal(null)

return (
<div
key={bucket.id}
style={style}
className={cn(
'group flex items-center justify-between rounded-md',
isSelected && 'text-foreground bg-surface-100'
isSelected && 'text-foreground bg-surface-100',
className
)}
>
{/* Even though we trim whitespaces from bucket names, there may be some existing buckets with trailing whitespaces. */}
Expand Down
120 changes: 120 additions & 0 deletions apps/studio/components/interfaces/Storage/StorageMenu.BucketList.tsx
Original file line number Diff line number Diff line change
@@ -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<BucketListProps>) => {
const bucket = data.buckets[index]
const isSelected = data.selectedBucketId === bucket.id

return (
<BucketRow
bucket={bucket}
isSelected={isSelected}
projectRef={data.projectRef}
style={style as CSSProperties}
className={BUCKET_ROW_HEIGHT}
/>
)
},
(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<HTMLDivElement>(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<BucketListProps>(
() => ({
buckets,
projectRef,
selectedBucketId,
}),
[buckets, projectRef, selectedBucketId]
)

return (
<div ref={sizerRef} className="flex-grow">
<List
itemCount={buckets.length}
itemData={itemData}
itemKey={(index) => buckets[index].id}
height={listHeight}
// itemSize should match the height of BucketRow + any gap/margin
itemSize={28}
width="100%"
>
{VirtualizedBucketRow}
</List>
</div>
)
}

export const BucketList = ({ buckets, selectedBucketId, projectRef = '' }: BucketListProps) => {
const numBuckets = buckets.length

if (numBuckets <= 50) {
return (
<div className="mr-3 mb-6">
{buckets.map((bucket) => (
<BucketRow
key={bucket.id}
bucket={bucket}
isSelected={selectedBucketId === bucket.id}
projectRef={projectRef}
className={BUCKET_ROW_HEIGHT}
/>
))}
</div>
)
}

return (
<BucketListVirtualized
buckets={buckets}
selectedBucketId={selectedBucketId}
projectRef={projectRef}
/>
)
}
58 changes: 32 additions & 26 deletions apps/studio/components/interfaces/Storage/StorageMenu.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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()
Expand Down Expand Up @@ -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 (
<>
<Menu type="pills" className="mt-6 flex flex-grow flex-col">
<Menu
type="pills"
className="pt-6 h-full flex flex-col"
ulClassName="flex flex-col flex-grow"
>
<div className="mb-6 mx-5 flex flex-col gap-y-1.5">
<CreateBucketModal />

Expand Down Expand Up @@ -100,8 +110,8 @@ export const StorageMenu = () => {
</InnerSideBarFilters>
</div>

<div className="space-y-6">
<div className="mx-3">
<div className="flex flex-col flex-grow">
<div className="flex-grow ml-3 flex flex-col">
<Menu.Group title={<span className="uppercase font-mono">All buckets</span>} />

{isLoading && (
Expand Down Expand Up @@ -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 (
<BucketRow
key={`${idx}_${bucket.id}`}
bucket={bucket}
projectRef={ref}
isSelected={isSelected}
/>
)
})}
{filteredBuckets.length > 0 && (
<BucketList
buckets={filteredBuckets}
selectedBucketId={bucketId}
projectRef={ref}
/>
)}
</>
)}
</div>
Expand Down
17 changes: 6 additions & 11 deletions e2e/studio/.env.local.example
Original file line number Diff line number Diff line change
@@ -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=
Loading
Loading