diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index 3129c172d0e66..3bf86dd9ee2fe 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -13,9 +13,9 @@ import { useTableEditorStateSnapshot } from 'state/table-editor' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' import { Shortcuts } from './components/common/Shortcuts' -import Footer from './components/footer/Footer' +import { Footer } from './components/footer/Footer' import { Grid } from './components/grid/Grid' -import Header, { HeaderProps } from './components/header/Header' +import { Header, HeaderProps } from './components/header/Header' import { RowContextMenu } from './components/menu' import { GridProps } from './types' @@ -84,7 +84,7 @@ export const SupabaseGrid = ({ return (
-
+
{children || ( <> @@ -99,7 +99,7 @@ export const SupabaseGrid = ({ filters={filters} onApplyFilters={onApplyFilters} /> -
+
)} diff --git a/apps/studio/components/grid/components/footer/Footer.tsx b/apps/studio/components/grid/components/footer/Footer.tsx index f4680d0acacb2..a6281989b9c03 100644 --- a/apps/studio/components/grid/components/footer/Footer.tsx +++ b/apps/studio/components/grid/components/footer/Footer.tsx @@ -5,14 +5,9 @@ import { useTableEditorQuery } from 'data/table-editor/table-editor-query' import { isTableLike, isViewLike } from 'data/table-editor/table-editor-types' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useUrlState } from 'hooks/ui/useUrlState' -import RefreshButton from '../header/RefreshButton' import { Pagination } from './pagination' -export interface FooterProps { - isRefetching?: boolean -} - -const Footer = ({ isRefetching }: FooterProps) => { +export const Footer = () => { const { id: _id } = useParams() const id = _id ? Number(_id) : undefined const { data: project } = useSelectedProjectQuery() @@ -41,10 +36,6 @@ const Footer = ({ isRefetching }: FooterProps) => { {selectedView === 'data' && }
- {entity && selectedView === 'data' && ( - - )} - {(isViewSelected || isTableSelected) && ( { ) } - -export default Footer diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index fde164a247cc5..821a359c06d11 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -8,7 +8,7 @@ import { toast } from 'sonner' import { useParams } from 'common' import { useTableFilter } from 'components/grid/hooks/useTableFilter' import { useTableSort } from 'components/grid/hooks/useTableSort' -import GridHeaderActions from 'components/interfaces/TableGridEditor/GridHeaderActions' +import { GridHeaderActions } from 'components/interfaces/TableGridEditor/GridHeaderActions' import { formatTableRowsToSQL } from 'components/interfaces/TableGridEditor/TableEntity.utils' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query' @@ -56,9 +56,10 @@ export const MAX_EXPORT_ROW_COUNT_MESSAGE = ( export type HeaderProps = { customHeader: ReactNode + isRefetching: boolean } -const Header = ({ customHeader }: HeaderProps) => { +export const Header = ({ customHeader, isRefetching }: HeaderProps) => { const snap = useTableEditorTableStateSnapshot() return ( @@ -71,14 +72,12 @@ const Header = ({ customHeader }: HeaderProps) => { ) : ( )} - +
) } -export default Header - const DefaultHeader = () => { const { ref: projectRef } = useParams() const { data: org } = useSelectedOrganizationQuery() diff --git a/apps/studio/components/grid/components/header/RefreshButton.tsx b/apps/studio/components/grid/components/header/RefreshButton.tsx index 1020e419b0235..91c611c8c9ad0 100644 --- a/apps/studio/components/grid/components/header/RefreshButton.tsx +++ b/apps/studio/components/grid/components/header/RefreshButton.tsx @@ -2,15 +2,15 @@ import { useQueryClient } from '@tanstack/react-query' import { RefreshCw } from 'lucide-react' import { useParams } from 'common' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { tableRowKeys } from 'data/table-rows/keys' -import { Button } from 'ui' export type RefreshButtonProps = { tableId?: number isRefetching?: boolean } -const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) => { +export const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) => { const { ref } = useParams() const queryClient = useQueryClient() const queryKey = tableRowKeys.tableRowsAndCount(ref, tableId) @@ -20,14 +20,18 @@ const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) => { } return ( - + className="w-7 h-7 p-0" + tooltip={{ + content: { + side: 'bottom', + text: 'Refresh table data', + }, + }} + /> ) } -export default RefreshButton diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index 0ead1d0a20090..49c683f70b236 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -8,7 +8,7 @@ import { toast } from 'sonner' import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import AlertError from 'components/ui/AlertError' -import APIDocsButton from 'components/ui/APIDocsButton' +import { APIDocsButton } from 'components/ui/APIDocsButton' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { FilterPopover } from 'components/ui/FilterPopover' import { FormHeader } from 'components/ui/Forms/FormHeader' diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 9ffa0387be786..9fb2103792bf8 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -46,9 +46,6 @@ import { Input_Shadcn_, Label_Shadcn_ as Label, Switch, - Tooltip, - TooltipContent, - TooltipTrigger, cn, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -92,7 +89,11 @@ export const CreateBranchModal = () => { useCheckGithubBranchValidity({ onError: () => {}, }) - const { data: cloneBackups, error: cloneBackupsError } = useCloneBackupsQuery( + const { + data: cloneBackups, + error: cloneBackupsError, + isLoading: isLoadingCloneBackups, + } = useCloneBackupsQuery( { projectRef }, { // [Joshen] Only trigger this request when the modal is opened @@ -359,26 +360,29 @@ export const CreateBranchModal = () => { name="withData" render={({ field }) => ( + + {!disableBackupsCheck && (isLoadingCloneBackups || noPhysicalBackups) && ( + + Requires PITR + + )} + + } layout="flex-row-reverse" + className="[&>div>label]:mb-1" description="Clone production data into this branch" > - - - - - - - {!disableBackupsCheck && noPhysicalBackups && ( - - PITR is required for the project to clone data into the branch - - )} - + + + )} /> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx index 6557966d680f8..ed79e6d5687ca 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx @@ -4,11 +4,11 @@ import { PricingMetric } from 'data/analytics/org-daily-stats-query' import type { OrgSubscription } from 'data/subscriptions/types' import type { OrgUsageResponse } from 'data/usage/org-usage-query' import { formatCurrency } from 'lib/helpers' +import { ChevronRight } from 'lucide-react' import { useMemo } from 'react' -import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from 'ui' +import { Button, cn, HoverCard, HoverCardContent, HoverCardTrigger } from 'ui' import { billingMetricUnit, formatUsage } from '../helpers' import { Metric, USAGE_APPROACHING_THRESHOLD } from './BillingBreakdown.constants' -import { ChevronRight } from 'lucide-react' export interface BillingMetricProps { idx: number @@ -17,14 +17,16 @@ export interface BillingMetricProps { usage: OrgUsageResponse subscription: OrgSubscription relativeToSubscription: boolean + className?: string } -const BillingMetric = ({ +export const BillingMetric = ({ slug, metric, usage, subscription, relativeToSubscription, + className, }: BillingMetricProps) => { const usageMeta = usage.usages.find((x) => x.metric === metric.key) @@ -79,31 +81,41 @@ const BillingMetric = ({ return ( -
- - {metric.anchor ? ( +
+ {metric.anchor ? ( +

{metric.name}

{usageMeta.available_in_plan && ( - + )}
- ) : ( -

{metric.name}

- )} - {usageLabel}  - {relativeToSubscription && usageMeta.cost && usageMeta.cost > 0 ? ( - - ({formatCurrency(usageMeta.cost)}) - - ) : usageMeta.available_in_plan && !usageMeta.unlimited && relativeToSubscription ? ( - {percentageLabel} - ) : null} - + {usageLabel}  + {relativeToSubscription && usageMeta.cost && usageMeta.cost > 0 ? ( + + ({formatCurrency(usageMeta.cost)}) + + ) : usageMeta.available_in_plan && !usageMeta.unlimited && relativeToSubscription ? ( + {percentageLabel} + ) : null} + + ) : ( +
+

{metric.name}

+ {usageLabel}  + {relativeToSubscription && usageMeta.cost && usageMeta.cost > 0 ? ( + + ({formatCurrency(usageMeta.cost)}) + + ) : usageMeta.available_in_plan && !usageMeta.unlimited && relativeToSubscription ? ( + {percentageLabel} + ) : null} +
+ )} {usageMeta.available_in_plan ? (
@@ -156,7 +168,7 @@ const BillingMetric = ({
{usageMeta.available_in_plan && ( - +

{usageMeta.unit_price_desc} @@ -229,5 +241,3 @@ const BillingMetric = ({ ) } - -export default BillingMetric diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/ComputeMetric.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/ComputeMetric.tsx index 16f62d8614e8c..df14738bfd77e 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/ComputeMetric.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/ComputeMetric.tsx @@ -3,20 +3,27 @@ import Link from 'next/link' import { ComputeUsageMetric, PricingMetric } from 'data/analytics/org-daily-stats-query' import type { OrgUsageResponse } from 'data/usage/org-usage-query' import { formatCurrency } from 'lib/helpers' +import { ChevronRight } from 'lucide-react' import { useMemo } from 'react' +import { HoverCard, HoverCardContent, HoverCardTrigger } from 'ui' import { formatUsage } from '../helpers' import { Metric } from './BillingBreakdown.constants' -import { ChevronRight } from 'lucide-react' -import { HoverCard, HoverCardContent, HoverCardTrigger } from 'ui' export interface ComputeMetricProps { slug?: string metric: Metric usage: OrgUsageResponse relativeToSubscription: boolean + className?: string } -const ComputeMetric = ({ slug, metric, usage, relativeToSubscription }: ComputeMetricProps) => { +export const ComputeMetric = ({ + slug, + metric, + usage, + relativeToSubscription, + className, +}: ComputeMetricProps) => { const usageMeta = usage.usages.find((x) => x.metric === metric.key) const usageLabel = useMemo(() => { @@ -37,8 +44,8 @@ const ComputeMetric = ({ slug, metric, usage, relativeToSubscription }: ComputeM return ( - -

+ +

@@ -55,7 +62,7 @@ const ComputeMetric = ({ slug, metric, usage, relativeToSubscription }: ComputeM ) : null}

- +

{usageMeta?.unit_price_desc} @@ -126,5 +133,3 @@ const ComputeMetric = ({ slug, metric, usage, relativeToSubscription }: ComputeM ) } - -export default ComputeMetric diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx index 98e84f71649c2..7440ff506fb57 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx @@ -32,39 +32,18 @@ interface BillingCustomerDataFormProps { } // Define the expected form values structure and validation schema -export const BillingCustomerDataSchema = z - .object({ - billing_name: z.string().min(3, 'Name must be at least 3 letters long'), - line1: z.string().optional(), - line2: z.string().optional(), - city: z.string().optional(), - state: z.string().optional(), - postal_code: z.string().optional(), - country: z.string().optional(), - tax_id_type: z.string(), - tax_id_value: z.string(), - tax_id_name: z.string(), - }) - .refine( - (data) => { - // its fine to just set the name, but once any other field is set, requires full address - const hasAnyField = data.line1 || data.line2 || data.city || data.state || data.postal_code - // If any field has value, country and line1 must have values. - return !hasAnyField || (!!data.country && !!data.line1) - }, - { - message: 'Country and Address line 1 are required if any other field is provided.', - path: ['line1'], - } - ) - .refine((data) => !(!!data.line1 && !data.country), { - message: 'Please select a country', - path: ['country'], - }) - .refine((data) => !(!!data.country && !data.line1), { - message: 'Please provide an address line 1', - path: ['line1'], - }) +export const BillingCustomerDataSchema = z.object({ + billing_name: z.string().min(3, 'Name must be at least 3 letters long'), + line1: z.string().trim().min(3, 'Address line 1 is required'), + line2: z.string().optional(), + city: z.string().trim().min(2, 'City is required'), + state: z.string().trim(), + postal_code: z.string().trim().min(1, 'Postal code is required'), + country: z.string().trim().min(1, 'Country is required'), + tax_id_type: z.string(), + tax_id_value: z.string(), + tax_id_name: z.string(), +}) export type BillingCustomerDataFormValues = z.infer diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx index 114ed71e19f01..8f60d2a372f3b 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx @@ -63,8 +63,6 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection( }) const { data: taxId, isLoading: isCustomerTaxIdLoading } = useOrganizationTaxIdQuery({ slug }) - const hidePaymentMethodsWithoutAddress = useFlag('hidePaymentMethodsWithoutAddress') - const { data: allPaymentMethods, isLoading } = useOrganizationPaymentMethodsQuery({ slug }) const paymentMethods = useMemo(() => { @@ -74,18 +72,16 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection( defaultPaymentMethodId: null, } - const filtered = allPaymentMethods.data.filter( - (pm) => !hidePaymentMethodsWithoutAddress || pm.has_address - ) return { - data: filtered, + // force customer to put down address via payment method creation flow if they don't have an address set + data: customerProfile?.address == null ? [] : allPaymentMethods.data, defaultPaymentMethodId: allPaymentMethods.data.some( (pm) => pm.id === allPaymentMethods.defaultPaymentMethodId ) ? allPaymentMethods.defaultPaymentMethodId : null, } - }, [allPaymentMethods]) + }, [allPaymentMethods, customerProfile]) const captchaRefCallback = useCallback((node: any) => { setCaptchaRef(node) @@ -223,7 +219,7 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection( />

- {isLoading ? ( + {isLoading || isCustomerProfileLoading ? (

Retrieving payment methods

diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts index 7b2cb3e2d2703..bc3dfb716274c 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.ts @@ -42,9 +42,16 @@ export const formatMemberRoleToProjectRoleConfiguration = ( }) .filter(Boolean) .flat() - .filter( - (p) => p !== undefined && 'baseRoleId' in p && p.ref !== undefined - ) as ProjectRoleConfiguration[] + .filter((p) => { + // [Joshen] Validate only for project scoped roles + // This filters out project scoped roles for projects that the user doesn't have access to + // (e.g if the project was deleted) + if ('baseRoleId' in p!) { + return p.ref !== undefined + } else { + return p + } + }) as ProjectRoleConfiguration[] return roleConfiguration } diff --git a/apps/studio/components/interfaces/Organization/Usage/Compute.tsx b/apps/studio/components/interfaces/Organization/Usage/Compute.tsx index e32c1458b89c9..581f0ed11dd9f 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Compute.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Compute.tsx @@ -9,7 +9,7 @@ import { useOrgDailyComputeStatsQuery } from 'data/analytics/org-daily-compute-s import { ComputeUsageMetric, computeUsageMetricLabel } from 'data/analytics/org-daily-stats-query' import type { OrgSubscription } from 'data/subscriptions/types' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import SectionContent from './SectionContent' +import { SectionContent } from './SectionContent' import { Attribute, AttributeColor } from './Usage.constants' import UsageBarChart from './UsageBarChart' diff --git a/apps/studio/components/interfaces/Organization/Usage/SectionContent.tsx b/apps/studio/components/interfaces/Organization/Usage/SectionContent.tsx index ebabba92a8098..9d46db528ebf5 100644 --- a/apps/studio/components/interfaces/Organization/Usage/SectionContent.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/SectionContent.tsx @@ -1,16 +1,16 @@ import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold' +import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { PropsWithChildren } from 'react' import { Badge } from 'ui' import { CategoryAttribute } from './Usage.constants' -import { ExternalLink } from 'lucide-react' export interface SectionContent { section: Pick includedInPlan?: boolean } -const SectionContent = ({ +export const SectionContent = ({ section, includedInPlan, children, @@ -39,11 +39,13 @@ const SectionContent = ({
{links && links.length > 0 && (
-

More information

+

+ More information +

{links.map((link) => (
-
+

{link.name}

@@ -62,5 +64,3 @@ const SectionContent = ({ ) } - -export default SectionContent diff --git a/apps/studio/components/interfaces/Organization/Usage/SectionHeader.tsx b/apps/studio/components/interfaces/Organization/Usage/SectionHeader.tsx index f896bf6d4be51..06a345ea24a24 100644 --- a/apps/studio/components/interfaces/Organization/Usage/SectionHeader.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/SectionHeader.tsx @@ -1,11 +1,14 @@ +import { cn } from 'ui' + export interface SectionHeaderProps { title: string description: string + className?: string } -const SectionHeader = ({ title, description }: SectionHeaderProps) => { +const SectionHeader = ({ title, description, className }: SectionHeaderProps) => { return ( -
+

{title}

{description}

diff --git a/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx index c6329db4280a6..3995d22d4a92d 100644 --- a/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx @@ -12,9 +12,9 @@ import { useOrgUsageQuery } from 'data/usage/org-usage-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { cn } from 'ui' import { BILLING_BREAKDOWN_METRICS } from '../BillingSettings/BillingBreakdown/BillingBreakdown.constants' -import BillingMetric from '../BillingSettings/BillingBreakdown/BillingMetric' -import ComputeMetric from '../BillingSettings/BillingBreakdown/ComputeMetric' -import SectionContent from './SectionContent' +import { BillingMetric } from '../BillingSettings/BillingBreakdown/BillingMetric' +import { ComputeMetric } from '../BillingSettings/BillingBreakdown/ComputeMetric' +import { SectionContent } from './SectionContent' export interface ComputeProps { orgSlug: string @@ -58,7 +58,7 @@ export const TotalUsage = ({ }) // When the user filters by project ref or selects a custom timeframe, we only display usage+project breakdown, but no costs/limits - const showRelationToSubscription = currentBillingCycleSelected && !projectRef + const showRelationToSubscription = currentBillingCycleSelected && projectRef === 'all-projects' const hasExceededAnyLimits = showRelationToSubscription && @@ -186,12 +186,15 @@ export const TotalUsage = ({ )}
{sortedBillingMetrics.map((metric, i) => { + const isLastBillingMetric = i === sortedBillingMetrics.length - 1 + const isLastInRow = isLastBillingMetric && computeMetrics.length === 0 + return (
@@ -202,35 +205,41 @@ export const TotalUsage = ({ usage={usage} subscription={subscription!} relativeToSubscription={showRelationToSubscription} + className={cn(i % 2 === 0 ? 'md:pr-4' : 'md:pl-4')} />
) })} - {computeMetrics.map((metric, i) => ( -
- -
- ))} + {computeMetrics.map((metric, i) => { + return ( +
+ +
+ ) + })}
)} diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index 506e02109f978..728c45750b90f 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -4,11 +4,7 @@ import Link from 'next/link' import { useEffect, useMemo, useState } from 'react' import { useParams } from 'common' -import { - ScaffoldContainer, - ScaffoldContainerLegacy, - ScaffoldTitle, -} from 'components/layouts/Scaffold' +import { ScaffoldContainer, ScaffoldHeader, ScaffoldTitle } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import DateRangePicker from 'components/ui/DateRangePicker' import NoPermission from 'components/ui/NoPermission' @@ -18,7 +14,15 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants/metrics' -import { cn, Listbox } from 'ui' +import { + cn, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectGroup_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, +} from 'ui' import { Admonition } from 'ui-patterns' import { Restriction } from '../BillingSettings/Restriction' import Activity from './Activity' @@ -30,7 +34,7 @@ import { TotalUsage } from './TotalUsage' const Usage = () => { const { slug, projectRef } = useParams() const [dateRange, setDateRange] = useState() - const [selectedProjectRef, setSelectedProjectRef] = useState() + const [selectedProjectRef, setSelectedProjectRef] = useState('all-projects') const canReadSubscriptions = useCheckPermissions( PermissionAction.BILLING_READ, @@ -110,21 +114,33 @@ const Usage = () => { if (!canReadSubscriptions) { return ( - - - + + + + + ) } return ( <> - - Usage - -
- + + + Usage + + +
+
- {isLoadingSubscription && } + {isLoadingSubscription && ( +
+
+ + +
+ +
+ )} {isErrorSubscription && ( { )} {isSuccessSubscription && ( - <> - +
+
+ - { - if (value === 'all-projects') setSelectedProjectRef(undefined) - else setSelectedProjectRef(value) - }} - > - { + if (value === 'all-projects') setSelectedProjectRef('all-projects') + else setSelectedProjectRef(value) + }} > - All projects - - {orgProjects?.map((project) => ( - - {project.name} - - ))} - + + + + + + + All projects + + {orgProjects?.map((project) => ( + + {project.name} + + ))} + + + +
-
+

- Organization is on the {subscription.plan.name} plan + Organization is on the{' '} + {subscription.plan.name} Plan

+ + + + +

{billingCycleStart.format('DD MMM YYYY')} -{' '} {billingCycleEnd.format('DD MMM YYYY')}

- +
)}
- {selectedProjectRef ? ( + {selectedProjectRef && selectedProjectRef !== 'all-projects' ? ( - You are currently viewing usage for the - {selectedProject?.name || selectedProjectRef} project. Supabase uses{' '} + You are currently viewing usage for the{' '} + + {selectedProject?.name || selectedProjectRef} + {' '} + project. Supabase uses{' '} - + - - {categoryMeta.attributes.map((attribute) => attribute.key === 'diskSize' ? ( { - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') +export const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { const { columns, selectedItems, setSelectedItems, + setView, setSortBy, setSortByOrder, addNewFolderPlaceholder, } = useStorageExplorerStateSnapshot() - const snap = useStorageExplorerStateSnapshot() + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const onSelectCreateFolder = (columnIndex = -1) => { addNewFolderPlaceholder(columnIndex) @@ -80,10 +83,10 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { } arrow={} > - snap.setView(STORAGE_VIEWS.COLUMNS)}> + setView(STORAGE_VIEWS.COLUMNS)}> As columns - snap.setView(STORAGE_VIEWS.LIST)}> + setView(STORAGE_VIEWS.LIST)}> As list @@ -128,5 +131,3 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { ) } - -export default ColumnContextMenu diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx index 1e56e753cd8fe..b33474ac03d9f 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/CustomExpiryModal.tsx @@ -12,7 +12,7 @@ const unitMap = { years: 3600 * 24 * 365, } -const CustomExpiryModal = () => { +export const CustomExpiryModal = () => { const { onCopyUrl } = useCopyUrl() const snap = useStorageExplorerStateSnapshot() const { selectedFileCustomExpiry, setSelectedFileCustomExpiry } = snap @@ -100,5 +100,3 @@ const CustomExpiryModal = () => { ) } - -export default CustomExpiryModal diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx index 0c1442d0cbe12..2c3f7893e442c 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorer.tsx @@ -5,10 +5,10 @@ import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { cn } from 'ui' import { CONTEXT_MENU_KEYS, STORAGE_VIEWS } from '../Storage.constants' import type { StorageColumn, StorageItemWithColumn } from '../Storage.types' -import ColumnContextMenu from './ColumnContextMenu' -import FileExplorerColumn from './FileExplorerColumn' -import FolderContextMenu from './FolderContextMenu' -import ItemContextMenu from './ItemContextMenu' +import { ColumnContextMenu } from './ColumnContextMenu' +import { FileExplorerColumn } from './FileExplorerColumn' +import { FolderContextMenu } from './FolderContextMenu' +import { ItemContextMenu } from './ItemContextMenu' export interface FileExplorerProps { columns: StorageColumn[] @@ -20,7 +20,7 @@ export interface FileExplorerProps { onColumnLoadMore: (index: number, column: StorageColumn) => void } -const FileExplorer = ({ +export const FileExplorer = ({ columns = [], selectedItems = [], itemSearchString, @@ -90,5 +90,3 @@ const FileExplorer = ({
) } - -export default FileExplorer diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx index 5d1ed5f3066cd..e65e425cdb416 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx @@ -8,7 +8,7 @@ import { toast } from 'sonner' import InfiniteList from 'components/ui/InfiniteList' import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' @@ -20,7 +20,7 @@ import { STORAGE_VIEWS, } from '../Storage.constants' import type { StorageColumn, StorageItemWithColumn } from '../Storage.types' -import FileExplorerRow from './FileExplorerRow' +import { FileExplorerRow } from './FileExplorerRow' const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty }: any) => { return ( @@ -68,7 +68,7 @@ export interface FileExplorerColumnProps { onColumnLoadMore: (index: number, column: StorageColumn) => void } -const FileExplorerColumn = ({ +export const FileExplorerColumn = ({ index = 0, column, fullWidth = false, @@ -83,7 +83,10 @@ const FileExplorerColumn = ({ const fileExplorerColumnRef = useRef(null) const snap = useStorageExplorerStateSnapshot() - const canUpdateStorage = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateStorage } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) useEffect(() => { if (fileExplorerColumnRef) { @@ -275,5 +278,3 @@ const FileExplorerColumn = ({
) } - -export default FileExplorerColumn diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx index 915edffb0c11b..0d080f6c90f6b 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -1,11 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { compact, debounce, isEqual, noop } from 'lodash' -import { useCallback, useEffect, useRef, useState } from 'react' - -import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import APIDocsButton from 'components/ui/APIDocsButton' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Check, ChevronLeft, @@ -20,6 +14,12 @@ import { Upload, X, } from 'lucide-react' +import { useCallback, useEffect, useRef, useState } from 'react' + +import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { APIDocsButton } from 'components/ui/APIDocsButton' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { Button, @@ -137,10 +137,10 @@ const HeaderBreadcrumbs = ({ interface FileExplorerHeader { itemSearchString: string setItemSearchString: (value: string) => void - onFilesUpload: (event: any, columnIndex: number) => void + onFilesUpload: (event: any, columnIndex?: number) => void } -const FileExplorerHeader = ({ +export const FileExplorerHeader = ({ itemSearchString = '', setItemSearchString = noop, onFilesUpload = noop, @@ -179,7 +179,10 @@ const FileExplorerHeader = ({ const breadcrumbs = columns.map((column) => column.name) const backDisabled = columns.length <= 1 - const canUpdateStorage = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateStorage } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) useEffect(() => { if (itemSearchString) setSearchString(itemSearchString) @@ -423,7 +426,6 @@ const FileExplorerHeader = ({
- {/* @ts-ignore */}
) } - -export default FileExplorerHeader diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx index 5dea522960d90..9b4559c5b5b71 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx @@ -3,14 +3,17 @@ import { Download, Move, Trash2, X } from 'lucide-react' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { Button } from 'ui' import { downloadFile } from './StorageExplorer.utils' -const FileExplorerHeaderSelection = () => { +export const FileExplorerHeaderSelection = () => { const { ref: projectRef, bucketId } = useParams() - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const { selectedItems, @@ -80,5 +83,3 @@ const FileExplorerHeaderSelection = () => {
) } - -export default FileExplorerHeaderSelection diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx index 8979041793e68..df4a5eaa8ef1d 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx @@ -19,7 +19,7 @@ import SVG from 'react-inlinesvg' import { useParams } from 'common' import type { ItemRenderer } from 'components/ui/InfiniteList' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' @@ -47,7 +47,7 @@ import { URL_EXPIRY_DURATION, } from '../Storage.constants' import { StorageItem, StorageItemWithColumn } from '../Storage.types' -import FileExplorerRowEditing from './FileExplorerRowEditing' +import { FileExplorerRowEditing } from './FileExplorerRowEditing' import { copyPathToFolder, downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' @@ -98,13 +98,13 @@ export const RowIcon = ({ return } -export interface FileExplorerRowProps { +interface FileExplorerRowProps { view: STORAGE_VIEWS columnIndex: number selectedItems: StorageItemWithColumn[] } -const FileExplorerRow: ItemRenderer = ({ +export const FileExplorerRow: ItemRenderer = ({ index: itemIndex, item, view = STORAGE_VIEWS.COLUMNS, @@ -139,7 +139,10 @@ const FileExplorerRow: ItemRenderer = ({ const isOpened = openedFolders.length > columnIndex ? openedFolders[columnIndex].name === item.name : false const isPreviewed = !isEmpty(selectedFilePreview) && isEqual(selectedFilePreview?.id, item.id) - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const onSelectFile = async (columnIndex: number, file: StorageItem) => { popColumnAtIndex(columnIndex) @@ -455,5 +458,3 @@ const FileExplorerRow: ItemRenderer = ({
) } - -export default FileExplorerRow diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx index 415e2b89cdb39..2ee1a7f02c31e 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx @@ -12,7 +12,11 @@ export interface FileExplorerRowEditingProps { columnIndex: number } -const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEditingProps) => { +export const FileExplorerRowEditing = ({ + item, + view, + columnIndex, +}: FileExplorerRowEditingProps) => { const { renameFile, renameFolder, addNewFolder, updateRowStatus } = useStorageExplorerStateSnapshot() @@ -112,5 +116,3 @@ const FileExplorerRowEditing = ({ item, view, columnIndex }: FileExplorerRowEdit
) } - -export default FileExplorerRowEditing diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx index d4b2dafff541c..1242a841a7b62 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx @@ -3,7 +3,7 @@ import { Clipboard, Download, Edit, Trash2 } from 'lucide-react' import { Item, Menu, Separator } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { copyPathToFolder } from './StorageExplorer.utils' @@ -11,10 +11,13 @@ interface FolderContextMenuProps { id: string } -const FolderContextMenu = ({ id = '' }: FolderContextMenuProps) => { +export const FolderContextMenu = ({ id = '' }: FolderContextMenuProps) => { const { openedFolders, downloadFolder, setSelectedItemToRename, setSelectedItemsToDelete } = useStorageExplorerStateSnapshot() - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) return ( @@ -42,5 +45,3 @@ const FolderContextMenu = ({ id = '' }: FolderContextMenuProps) => { ) } - -export default FolderContextMenu diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx index 4ce816e86aacb..0712e4b6578c7 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx @@ -4,7 +4,7 @@ import { Item, Menu, Separator, Submenu } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' import { useParams } from 'common' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { URL_EXPIRY_DURATION } from '../Storage.constants' import { StorageItemWithColumn } from '../Storage.types' @@ -15,7 +15,7 @@ interface ItemContextMenuProps { id: string } -const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { +export const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { const { ref: projectRef, bucketId } = useParams() const snap = useStorageExplorerStateSnapshot() const { setSelectedFileCustomExpiry } = snap @@ -28,7 +28,10 @@ const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { } = useStorageExplorerStateSnapshot() const { onCopyUrl } = useCopyUrl() const isPublic = selectedBucket.public - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) const onHandleClick = async (event: any, item: StorageItemWithColumn, expiresIn?: number) => { if (item.isCorrupted) return @@ -106,5 +109,3 @@ const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { ) } - -export default ItemContextMenu diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx index 9ec26ea001e21..8da76072e4235 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx @@ -12,7 +12,7 @@ interface MoveItemsModalProps { onSelectMove: (path: string) => void } -const MoveItemsModal = ({ +export const MoveItemsModal = ({ bucketName = '', visible = false, selectedItemsToMove = [], @@ -87,5 +87,3 @@ const MoveItemsModal = ({ ) } - -export default MoveItemsModal diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx index a7f39873b676f..44adc6c0b305a 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx @@ -6,7 +6,7 @@ import SVG from 'react-inlinesvg' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' @@ -115,7 +115,7 @@ const PreviewFile = ({ item }: { item: StorageItem }) => { ) } -const PreviewPane = () => { +export const PreviewPane = () => { const { ref: projectRef, bucketId } = useParams() const { @@ -127,7 +127,10 @@ const PreviewPane = () => { } = useStorageExplorerStateSnapshot() const { onCopyUrl } = useCopyUrl() - const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') + const { can: canUpdateFiles } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_WRITE, + '*' + ) if (!file) return null @@ -139,148 +142,144 @@ const PreviewPane = () => { const updatedAt = file.updated_at ? new Date(file.updated_at).toLocaleString() : 'Unknown' return ( - <> - -
- {/* Preview Header */} -
- setSelectedFilePreview(undefined)} - /> -
+ +
+ {/* Preview Header */} +
+ setSelectedFilePreview(undefined)} + /> +
- {/* Preview Thumbnail*/} -
-
- -
+ {/* Preview Thumbnail*/} +
+
+
+
-
- {/* Preview Information */} -
-
{file.name}
- {file.isCorrupted && ( -
- -

- File is corrupted, please delete and reupload this file again -

-
- )} - {mimeType && ( +
+ {/* Preview Information */} +
+
{file.name}
+ {file.isCorrupted && ( +
+

- {mimeType} - {size && - {size}} + File is corrupted, please delete and reupload this file again

- )} -
- - {/* Preview Metadata */} -
-
- -

{createdAt}

-
-
- -

{updatedAt}

+ )} + {mimeType && ( +

+ {mimeType} + {size && - {size}} +

+ )} +
+ + {/* Preview Metadata */} +
+
+ +

{createdAt}

+
+
+ +

{updatedAt}

+
- {/* Actions */} -
+ {/* Actions */} +
+ + {selectedBucket.public ? ( - {selectedBucket.public ? ( - - ) : ( - - - - - - onCopyUrl(file.name, URL_EXPIRY_DURATION.WEEK)} - > - Expire in 1 week - - onCopyUrl(file.name, URL_EXPIRY_DURATION.MONTH)} - > - Expire in 1 month - - onCopyUrl(file.name, URL_EXPIRY_DURATION.YEAR)} - > - Expire in 1 year - - setSelectedFileCustomExpiry(file)} - > - Custom expiry - - - - )} -
- } - onClick={() => setSelectedItemsToDelete([file])} - tooltip={{ - content: { - side: 'bottom', - text: !canUpdateFiles - ? 'You need additional permissions to delete this file' - : undefined, - }, - }} - > - Delete file - + ) : ( + + + + + + onCopyUrl(file.name, URL_EXPIRY_DURATION.WEEK)} + > + Expire in 1 week + + onCopyUrl(file.name, URL_EXPIRY_DURATION.MONTH)} + > + Expire in 1 month + + onCopyUrl(file.name, URL_EXPIRY_DURATION.YEAR)} + > + Expire in 1 year + + setSelectedFileCustomExpiry(file)} + > + Custom expiry + + + + )}
+ } + onClick={() => setSelectedItemsToDelete([file])} + tooltip={{ + content: { + side: 'bottom', + text: !canUpdateFiles + ? 'You need additional permissions to delete this file' + : undefined, + }, + }} + > + Delete file +
- - +
+ ) } - -export default PreviewPane diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx index e352dc2b9f7a7..d3616e59a6550 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/StorageExplorer.tsx @@ -8,12 +8,12 @@ import { IS_PLATFORM } from 'lib/constants' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' import { ConfirmDeleteModal } from './ConfirmDeleteModal' -import CustomExpiryModal from './CustomExpiryModal' -import FileExplorer from './FileExplorer' -import FileExplorerHeader from './FileExplorerHeader' -import FileExplorerHeaderSelection from './FileExplorerHeaderSelection' -import MoveItemsModal from './MoveItemsModal' -import PreviewPane from './PreviewPane' +import { CustomExpiryModal } from './CustomExpiryModal' +import { FileExplorer } from './FileExplorer' +import { FileExplorerHeader } from './FileExplorerHeader' +import { FileExplorerHeaderSelection } from './FileExplorerHeaderSelection' +import { MoveItemsModal } from './MoveItemsModal' +import { PreviewPane } from './PreviewPane' interface StorageExplorerProps { bucket: Bucket @@ -126,7 +126,7 @@ export const StorageExplorer = ({ bucket }: StorageExplorerProps) => { /** File manipulation methods */ - const onFilesUpload = async (event: any, columnIndex = -1) => { + const onFilesUpload = async (event: any, columnIndex: number = -1) => { event.persist() const items = event.target.files || event.dataTransfer.items const isDrop = !isEmpty(get(event, ['dataTransfer', 'items'], [])) diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx index 1dffed3424640..dc71182c82fe8 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketRow.tsx @@ -23,7 +23,7 @@ interface StoragePoliciesBucketRowProps { label: string bucket?: Bucket policies: PostgresPolicy[] - onSelectPolicyAdd: (bucketName: string, table: string) => void + onSelectPolicyAdd: (bucketName: string | undefined, table: string) => void onSelectPolicyEdit: (policy: PostgresPolicy, bucketName: string, table: string) => void onSelectPolicyDelete: (policy: PostgresPolicy) => void } @@ -45,11 +45,9 @@ export const StoragePoliciesBucketRow = ({ {label} {bucket?.public && Public}
- {!!bucket && ( - - )} + {policies.length === 0 ? ( diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx index 8d331ac886dec..cb5b6de2ab9b8 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/CreateCredentialModal.tsx @@ -9,7 +9,7 @@ import { useParams } from 'common' import { useIsProjectActive } from 'components/layouts/ProjectLayout/ProjectContext' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useS3AccessKeyCreateMutation } from 'data/storage/s3-access-key-create-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, Dialog, @@ -39,7 +39,10 @@ export const CreateCredentialModal = ({ visible, onOpenChange }: CreateCredentia const isProjectActive = useIsProjectActive() const [showSuccess, setShowSuccess] = useState(false) - const canCreateCredentials = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const { can: canCreateCredentials } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_ADMIN_WRITE, + '*' + ) const { data: config } = useProjectStorageConfigQuery({ projectRef }) const isS3ConnectionEnabled = config?.features.s3Protocol.enabled diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx index 85085e00e88f7..0057b4e751d4b 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/S3Connection.tsx @@ -22,7 +22,7 @@ import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation' import { useStorageCredentialsQuery } from 'data/storage/s3-access-key-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { AlertDescription_Shadcn_, @@ -54,8 +54,12 @@ export const S3Connection = () => { const [openDeleteDialog, setOpenDeleteDialog] = useState(false) const [deleteCred, setDeleteCred] = useState<{ id: string; description: string }>() - const canReadS3Credentials = useCheckPermissions(PermissionAction.STORAGE_ADMIN_READ, '*') - const canUpdateStorageSettings = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const { can: canReadS3Credentials, isLoading: isLoadingPermissions } = + useAsyncCheckProjectPermissions(PermissionAction.STORAGE_ADMIN_READ, '*') + const { can: canUpdateStorageSettings } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_ADMIN_WRITE, + '*' + ) const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { @@ -170,7 +174,7 @@ export const S3Connection = () => {
- {!canUpdateStorageSettings && ( + {!isLoadingPermissions && !canUpdateStorageSettings && (

You need additional permissions to update storage settings @@ -216,6 +220,7 @@ export const S3Connection = () => { +

@@ -227,10 +232,10 @@ export const S3Connection = () => {
- {!canReadS3Credentials ? ( - - ) : projectIsLoading ? ( + {projectIsLoading || isLoadingPermissions ? ( + ) : !canReadS3Credentials ? ( + ) : !isProjectActive ? ( diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx index d1dd01b9b1bf1..7eb0717b6ff70 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/StorageCredItem.tsx @@ -3,7 +3,7 @@ import { differenceInDays } from 'date-fns' import { MoreVertical, TrashIcon } from 'lucide-react' import CopyButton from 'components/ui/CopyButton' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { Button, DropdownMenu, @@ -25,7 +25,10 @@ export const StorageCredItem = ({ access_key: string onDeleteClick: (id: string) => void }) => { - const canRemoveAccessKey = useCheckPermissions(PermissionAction.STORAGE_ADMIN_WRITE, '*') + const { can: canRemoveAccessKey } = useAsyncCheckProjectPermissions( + PermissionAction.STORAGE_ADMIN_WRITE, + '*' + ) function daysSince(date: string) { const now = new Date() diff --git a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx index c4dab21d894c7..0322cba739a24 100644 --- a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx @@ -5,8 +5,9 @@ import { useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' +import { RefreshButton } from 'components/grid/components/header/RefreshButton' import { getEntityLintDetails } from 'components/interfaces/TableGridEditor/TableEntity.utils' -import APIDocsButton from 'components/ui/APIDocsButton' +import { APIDocsButton } from 'components/ui/APIDocsButton' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query' @@ -45,9 +46,10 @@ import ViewEntityAutofixSecurityModal from './ViewEntityAutofixSecurityModal' export interface GridHeaderActionsProps { table: Entity + isRefetching: boolean } -const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { +export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProps) => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() @@ -277,13 +279,13 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { -

+

Row Level Security (RLS) -

-
+ +

You can restrict and control who can read, write and update data in this table using Row Level Security. @@ -293,14 +295,13 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { table.

{!isSchemaLocked && ( -
- -
+ )}
@@ -443,8 +444,9 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => { {isTable && realtimeEnabled && ( - + {!isRealtimeEnabled && 'Enable Realtime'} + )} + {doesHaveAutoGeneratedAPIDocs && } + +
)} {
) } - -export default GridHeaderActions diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx index 9a2a6ac1c87df..e815f7c3873f8 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx @@ -4,7 +4,7 @@ import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' import { useParams } from 'common' -import RefreshButton from 'components/grid/components/header/RefreshButton' +import { RefreshButton } from 'components/grid/components/header/RefreshButton' import { FilterPopoverPrimitive } from 'components/grid/components/header/filter/FilterPopoverPrimitive' import { SortPopoverPrimitive } from 'components/grid/components/header/sort/SortPopoverPrimitive' import type { Filter, Sort } from 'components/grid/types' diff --git a/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx b/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx index 45e81d6abf84e..f424dc65ba47c 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' import { useMemo, useRef } from 'react' import { useParams } from 'common' -import Footer from 'components/grid/components/footer/Footer' +import { Footer } from 'components/grid/components/footer/Footer' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useTableDefinitionQuery } from 'data/database/table-definition-query' import { useViewDefinitionQuery } from 'data/database/view-definition-query' @@ -24,7 +24,7 @@ export interface TableDefinitionProps { entity?: Entity } -const TableDefinition = ({ entity }: TableDefinitionProps) => { +export const TableDefinition = ({ entity }: TableDefinitionProps) => { const { ref } = useParams() const editorRef = useRef(null) const monacoRef = useRef(null) @@ -132,5 +132,3 @@ const TableDefinition = ({ entity }: TableDefinitionProps) => { ) } - -export default TableDefinition diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index d1bff23294c1b..f0c1904650a5f 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -22,7 +22,7 @@ import { Button } from 'ui' import { Admonition, GenericSkeletonLoader } from 'ui-patterns' import DeleteConfirmationDialogs from './DeleteConfirmationDialogs' import SidePanelEditor from './SidePanelEditor/SidePanelEditor' -import TableDefinition from './TableDefinition' +import { TableDefinition } from './TableDefinition' export interface TableGridEditorProps { isLoadingSelectedTable?: boolean diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx index ae035f651381e..ef94be5b6d26e 100644 --- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx +++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx @@ -9,7 +9,7 @@ import { useParams } from 'common' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { EdgeFunctionTesterSheet } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet' import { PageLayout } from 'components/layouts/PageLayout/PageLayout' -import APIDocsButton from 'components/ui/APIDocsButton' +import { APIDocsButton } from 'components/ui/APIDocsButton' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' import { useEdgeFunctionBodyQuery } from 'data/edge-functions/edge-function-body-query' diff --git a/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx b/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx index 0ba928c7ed520..d82d07658b797 100644 --- a/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/PauseFailedState.tsx @@ -7,20 +7,25 @@ import { useParams } from 'common' import { DeleteProjectModal } from 'components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' +import { InlineLink } from 'components/ui/InlineLink' import { useBackupDownloadMutation } from 'data/database/backup-download-mutation' import { useDownloadableBackupQuery } from 'data/database/backup-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, CriticalIcon, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui' -const PauseFailedState = () => { +export const PauseFailedState = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const [visible, setVisible] = useState(false) - const canDeleteProject = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { project_id: project?.id }, - }) + const { can: canDeleteProject } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { + resource: { project_id: project?.id }, + } + ) const { data } = useDownloadableBackupQuery({ projectRef: ref }) const backups = data?.backups ?? [] @@ -57,7 +62,11 @@ const PauseFailedState = () => {

Something went wrong while pausing your project

Your project's data is intact, but your project is inaccessible due to the failure - while pausing. Please contact support for assistance. + while pausing. Database backups for this project can still be accessed{' '} + here. +

+

+ Please contact support for assistance.

@@ -78,7 +87,12 @@ const PauseFailedState = () => { tooltip={{ content: { side: 'bottom', - text: backups.length === 0 ? 'No available backups to download' : undefined, + text: + data?.status === 'physical-backups-enabled' + ? 'No available backups to download as project is on physical backups' + : backups.length === 0 + ? 'No available backups to download' + : undefined, }, }} onClick={onClickDownloadBackup} @@ -123,5 +137,3 @@ const PauseFailedState = () => { ) } - -export default PauseFailedState diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index 47e0c76f2b90b..90cd9ca68929d 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -24,12 +24,12 @@ import BuildingState from './BuildingState' import ConnectingState from './ConnectingState' import { LoadingState } from './LoadingState' import { ProjectPausedState } from './PausedState/ProjectPausedState' -import PauseFailedState from './PauseFailedState' +import { PauseFailedState } from './PauseFailedState' import PausingState from './PausingState' import ProductMenuBar from './ProductMenuBar' import { ResizingState } from './ResizingState' import RestartingState from './RestartingState' -import RestoreFailedState from './RestoreFailedState' +import { RestoreFailedState } from './RestoreFailedState' import RestoringState from './RestoringState' import { UpgradingState } from './UpgradingState' diff --git a/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx b/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx index 9120f3b2f4d38..edb62abd66c45 100644 --- a/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx @@ -7,20 +7,23 @@ import { useParams } from 'common' import { DeleteProjectModal } from 'components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip' +import { InlineLink } from 'components/ui/InlineLink' import { useBackupDownloadMutation } from 'data/database/backup-download-mutation' import { useDownloadableBackupQuery } from 'data/database/backup-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, CriticalIcon, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui' -const RestoreFailedState = () => { +export const RestoreFailedState = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const [visible, setVisible] = useState(false) - const canDeleteProject = useCheckPermissions(PermissionAction.UPDATE, 'projects', { - resource: { project_id: project?.id }, - }) + const { can: canDeleteProject } = useAsyncCheckProjectPermissions( + PermissionAction.UPDATE, + 'projects', + { resource: { project_id: project?.id } } + ) const { data } = useDownloadableBackupQuery({ projectRef: ref }) const backups = data?.backups ?? [] @@ -56,8 +59,12 @@ const RestoreFailedState = () => {

Something went wrong while restoring your project

- Your project's data is intact, but your project is inaccessible due to the - restoration failure. Please contact support for assistance. + Your project's data is intact, but your project is inaccessible due to a + restoration failure. Database backups for this project can still be accessed{' '} + here. +

+

+ Please contact support for assistance.

@@ -70,6 +77,7 @@ const RestoreFailedState = () => { Contact support + } @@ -78,16 +86,22 @@ const RestoreFailedState = () => { tooltip={{ content: { side: 'bottom', - text: backups.length === 0 ? 'No available backups to download' : undefined, + text: + data?.status === 'physical-backups-enabled' + ? 'No available backups to download as project is on physical backups' + : backups.length === 0 + ? 'No available backups to download' + : undefined, }, }} onClick={onClickDownloadBackup} > Download backup + - + icon={} + className="h-7 w-7" + tooltip={{ + content: { + side: 'bottom', + text: 'API Docs', + }, + }} + /> ) } - -export default APIDocsButton diff --git a/packages/shared-data/tweets.ts b/packages/shared-data/tweets.ts index fddb5b92e4232..cba89c1a39bf0 100644 --- a/packages/shared-data/tweets.ts +++ b/packages/shared-data/tweets.ts @@ -77,12 +77,6 @@ const tweets = [ handle: 'viratt_mankali', img_url: '/images/twitter-profiles/GtrVV2dD_400x400.jpg', }, - { - text: "Working with @supabase has been one of the best dev experiences I've had lately. Incredibly easy to set up, great documentation, and so many fewer hoops to jump through than the competition. I definitely plan to use it on any and all future projects.", - url: 'https://twitter.com/thatguy_tex/status/1497602628410388480', - handle: 'thatguy_tex', - img_url: '/images/twitter-profiles/09HouOSt_400x400.jpg', - }, { text: '@supabase is just 🤯 Now I see why a lot of people love using it as a backend for their applications. I am really impressed with how easy it is to set up an Auth and then just code it together for the frontend. @IngoKpp now I see your joy with Supabase #coding #fullstackwebdev', url: 'https://twitter.com/IxoyeDesign/status/1497473731777728512',