Merge branch 'master' of github.com:supabase/supabase

This commit is contained in:
Terry Sutton committed 2024-05-07 09:30:19 -02:30
commit 56a1babebf
42 files changed
+1385 -966

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
name: Config (Preview)
on:
pull_request:
types:
- opened
- reopened
- synchronize
branches:
- master
paths:
- "supabase/**"
workflow_dispatch:
jobs:
wait:
runs-on: ubuntu-latest
outputs:
status: ${{ steps.check.outputs.conclusion }}
steps:
- uses: fountainhead/action-wait-for-check@v1.2.0
id: check
with:
checkName: Supabase
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
apply:
needs:
- wait
if: ${{ needs.wait.outputs.status == 'success' }}
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
TF_VAR_linked_project: xguihxuzqibwxjnimxev
TF_VAR_git_branch: ${{ github.head_ref }}
TF_CLI_ARGS_apply: -target=supabase_settings.preview
defaults:
run:
working-directory: supabase/remotes
outputs:
db_user: ${{ steps.branch.outputs.user }}
db_pass: ${{ steps.branch.outputs.password }}
db_host: ${{ steps.branch.outputs.host }}
db_port: ${{ steps.branch.outputs.port }}
jwt_secret: ${{ steps.branch.outputs.jwt_secret }}
ref: ${{ steps.branch.outputs.id }}
status: ${{ steps.branch.outputs.status }}
version: ${{ steps.branch.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- run: terraform init
- run: terraform apply -auto-approve -no-color
- id: branch
run: |
terraform output -json branch_database \
| jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]" \
>> "$GITHUB_OUTPUT"
+28
View File
@@ -0,0 +1,28 @@
name: Config (Production)
on:
push:
branches:
- master
paths:
- "supabase/remotes/**"
workflow_dispatch:
jobs:
apply:
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
TF_VAR_linked_project: xguihxuzqibwxjnimxev
defaults:
run:
working-directory: supabase/remotes
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- run: terraform init
- run: terraform apply -auto-approve -no-color
@@ -1,3 +1,4 @@
import { Edit, MoreVertical, Search, Trash } from 'lucide-react'
import { useState } from 'react'
import {
Button,
@@ -5,10 +6,6 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconEdit,
IconMoreVertical,
IconSearch,
IconTrash,
Input,
} from 'ui'
@@ -45,12 +42,13 @@ const EnumeratedTypes = () => {
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const enumeratedTypes =
const enumeratedTypes = (data ?? []).filter((type) => type.enums.length > 0)
const filteredEnumeratedTypes =
search.length > 0
? (data ?? []).filter(
? enumeratedTypes.filter(
(x) => x.schema === selectedSchema && x.name.toLowerCase().includes(search.toLowerCase())
)
: (data ?? []).filter((x) => x.schema === selectedSchema)
: enumeratedTypes.filter((x) => x.schema === selectedSchema)
const protectedSchemas = (schemas ?? []).filter((schema) =>
EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
@@ -75,7 +73,7 @@ const EnumeratedTypes = () => {
className="w-64"
onChange={(e) => setSearch(e.target.value)}
placeholder="Search for a type"
icon={<IconSearch size={14} />}
icon={<Search size={14} />}
/>
</div>
{!isLocked && (
@@ -103,7 +101,7 @@ const EnumeratedTypes = () => {
]}
body={
<>
{enumeratedTypes.length === 0 && search.length === 0 && (
{filteredEnumeratedTypes.length === 0 && search.length === 0 && (
<Table.tr>
<Table.td colSpan={4}>
<p className="text-sm text-foreground">No enumerated types created yet</p>
@@ -113,7 +111,7 @@ const EnumeratedTypes = () => {
</Table.td>
</Table.tr>
)}
{enumeratedTypes.length === 0 && search.length > 0 && (
{filteredEnumeratedTypes.length === 0 && search.length > 0 && (
<Table.tr>
<Table.td colSpan={4}>
<p className="text-sm text-foreground">No results found</p>
@@ -123,8 +121,8 @@ const EnumeratedTypes = () => {
</Table.td>
</Table.tr>
)}
{enumeratedTypes.length > 0 &&
enumeratedTypes.map((type) => (
{filteredEnumeratedTypes.length > 0 &&
filteredEnumeratedTypes.map((type) => (
<Table.tr key={type.id}>
<Table.td className="w-20">
<p className="w-20 truncate">{type.schema}</p>
@@ -136,23 +134,21 @@ const EnumeratedTypes = () => {
<div className="flex justify-end items-center space-x-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1">
<IconMoreVertical />
</Button>
<Button type="default" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-32">
<DropdownMenuItem
className="space-x-2"
onClick={() => setSelectedTypeToEdit(type)}
>
<IconEdit size="tiny" />
<Edit size={14} />
<p>Update type</p>
</DropdownMenuItem>
<DropdownMenuItem
className="space-x-2"
onClick={() => setSelectedTypeToDelete(type)}
>
<IconTrash stroke="red" size="tiny" />
<Trash size={14} />
<p>Delete type</p>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -2,7 +2,6 @@ import Link from 'next/link'
import { useEffect, useMemo, useState } from 'react'
import toast from 'react-hot-toast'
import { SelectValue } from '@ui/components/shadcn/ui/select'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { CodeEditor } from 'components/ui/CodeEditor'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
@@ -11,9 +10,19 @@ import { useSchemasQuery } from 'data/database/schemas-query'
import { useTableColumnsQuery } from 'data/database/table-columns-query'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { Check, ChevronsUpDown } from 'lucide-react'
import {
Button,
Input,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
ScrollArea,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectSeparator_Shadcn_,
@@ -21,8 +30,10 @@ import {
SelectValue_Shadcn_,
Select_Shadcn_,
SidePanel,
cn,
} from 'ui'
import MultiSelect, { MultiSelectOption } from 'ui-patterns/MultiSelect'
import { MultiSelectOption } from 'ui-patterns/MultiSelect'
import { MultiSelectV2 } from 'ui-patterns/MultiSelect/MultiSelectV2'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { INDEX_TYPES } from './Indexes.constants'
@@ -37,6 +48,8 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) =
const [selectedEntity, setSelectedEntity] = useState<string | undefined>(undefined)
const [selectedColumns, setSelectedColumns] = useState<string[]>([])
const [selectedIndexType, setSelectedIndexType] = useState<string>(INDEX_TYPES[0].value)
const [schemaDropdownOpen, setSchemaDropdownOpen] = useState(false)
const [tableDropdownOpen, setTableDropdownOpen] = useState(false)
const { refetch: refetchIndexes } = useIndexesQuery({
schema: selectedSchema,
@@ -47,7 +60,7 @@ const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) =
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: entities } = useEntityTypesQuery({
const { data: entities, isLoading } = useEntityTypesQuery({
schema: selectedSchema,
sort: 'alphabetical',
search: undefined,
@@ -136,22 +149,62 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
<div className="py-6 space-y-6">
<SidePanel.Content className="space-y-6">
<FormItemLayout label="Select a schema" name="select-schema" isReactForm={false}>
<Select_Shadcn_
value={selectedSchema}
onValueChange={setSelectedSchema}
name="select-schema"
<Popover_Shadcn_
modal={false}
open={schemaDropdownOpen}
onOpenChange={setSchemaDropdownOpen}
>
<SelectTrigger_Shadcn_ size="small">
<SelectValue_Shadcn_>{selectedSchema}</SelectValue_Shadcn_>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{(schemas ?? []).map((schema) => (
<SelectItem_Shadcn_ key={schema.name} value={schema.name}>
{schema.name}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<Button
type="default"
size={'medium'}
className={`w-full [&>span]:w-full text-left`}
iconRight={
<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />
}
>
{selectedSchema !== undefined && selectedSchema !== ''
? selectedSchema
: 'Choose a schema'}
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
className="p-0"
side="bottom"
align="start"
sameWidthAsTrigger
>
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Find schema..." />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No schemas found</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
<ScrollArea className={(schemas || []).length > 7 ? 'h-[210px]' : ''}>
{(schemas ?? []).map((schema) => (
<CommandItem_Shadcn_
key={schema.name}
className="cursor-pointer flex items-center justify-between space-x-2 w-full"
onSelect={() => {
setSelectedSchema(schema.name)
setSchemaDropdownOpen(false)
}}
onClick={() => {
setSelectedSchema(schema.name)
setSchemaDropdownOpen(false)
}}
>
<span>{schema.name}</span>
{selectedEntity === schema.name && (
<Check className="text-brand" strokeWidth={2} size={16} />
)}
</CommandItem_Shadcn_>
))}
</ScrollArea>
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</FormItemLayout>
<FormItemLayout
@@ -159,42 +212,85 @@ CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexTyp
name="select-table"
description={
isSelectEntityDisabled &&
!isLoading &&
'Create a table in this schema via the Table or SQL editor first'
}
isReactForm={false}
>
<Select_Shadcn_
disabled={isSelectEntityDisabled}
value={selectedEntity}
onValueChange={setSelectedEntity}
name="select-table"
<Popover_Shadcn_
modal={false}
open={tableDropdownOpen}
onOpenChange={setTableDropdownOpen}
>
<SelectTrigger_Shadcn_ size="small">
<SelectValue
placeholder={
isSelectEntityDisabled ? 'No tables available in schema' : 'Choose a table'
}
>
{selectedEntity}
</SelectValue>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{(entityTypes ?? []).map((entity) => (
<SelectItem_Shadcn_ key={entity.name} value={entity.name}>
{entity.name}
</SelectItem_Shadcn_>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
<PopoverTrigger_Shadcn_ asChild disabled={isSelectEntityDisabled || isLoading}>
{isLoading ? (
<ShimmeringLoader className="h-[38px]" />
) : (
<Button
type="default"
size="medium"
className={cn(
'w-full [&>span]:w-full text-left',
selectedEntity === '' && 'text-foreground-lighter'
)}
iconRight={
<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />
}
>
{selectedEntity !== undefined && selectedEntity !== ''
? selectedEntity
: isSelectEntityDisabled
? 'No tables available in schema'
: 'Choose a table'}
</Button>
)}
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
className="p-0"
side="bottom"
align="start"
sameWidthAsTrigger
>
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Find table..." />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No tables found</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
<ScrollArea className={(entityTypes || []).length > 7 ? 'h-[210px]' : ''}>
{(entityTypes ?? []).map((entity) => (
<CommandItem_Shadcn_
key={entity.name}
className="cursor-pointer flex items-center justify-between space-x-2 w-full"
onSelect={() => {
setSelectedEntity(entity.name)
setTableDropdownOpen(false)
}}
onClick={() => {
setSelectedEntity(entity.name)
setTableDropdownOpen(false)
}}
>
<span>{entity.name}</span>
{selectedEntity === entity.name && (
<Check className="text-brand" strokeWidth={2} size={16} />
)}
</CommandItem_Shadcn_>
))}
</ScrollArea>
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</FormItemLayout>
{selectedEntity && (
<FormItemLayout label="Select up to 32 columns" isReactForm={false}>
{isLoadingTableColumns && <ShimmeringLoader className="py-4" />}
{isSuccessTableColumns && (
<MultiSelect
<MultiSelectV2
options={columnOptions}
placeholder=""
placeholder="Choose which columns to create an index on"
searchPlaceholder="Search for a column"
value={selectedColumns}
onChange={setSelectedColumns}
@@ -1,8 +1,8 @@
import { InformationCircleIcon } from '@heroicons/react/16/solid'
import { X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import toast from 'react-hot-toast'
import { X } from 'lucide-react'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -10,6 +10,7 @@ import { executeSql } from 'data/sql/execute-sql-query'
import { useLocalStorageQuery } from 'hooks'
import { DbQueryHook } from 'hooks/analytics/useDbQuery'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
Button,
TabsList_Shadcn_,
@@ -28,7 +29,6 @@ import { PresetHookResult } from '../Reports/Reports.utils'
import { QUERY_PERFORMANCE_REPORT_TYPES } from './QueryPerformance.constants'
import { QueryPerformanceFilterBar } from './QueryPerformanceFilterBar'
import { QueryPerformanceGrid } from './QueryPerformanceGrid'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
interface QueryPerformanceProps {
queryHitRate: PresetHookResult
@@ -1,11 +1,33 @@
import { X } from 'lucide-react'
import RGL, { WidthProvider } from 'react-grid-layout'
import { LAYOUT_COLUMN_COUNT } from './Reports.constants'
import { useParams } from 'common'
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { Button, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui'
import { LAYOUT_COLUMN_COUNT } from './Reports.constants'
const ReactGridLayout = WidthProvider(RGL)
const GridResize = ({ startDate, endDate, interval, editableReport, setEditableReport }: any) => {
if (!editableReport) return null
interface GridResizeProps {
startDate: string
endDate: string
interval: string
editableReport: any
onRemoveChart: ({ metric }: { metric: { key: string } }) => void
setEditableReport: (payload: any) => void
}
const GridResize = ({
startDate,
endDate,
interval,
editableReport,
onRemoveChart,
setEditableReport,
}: GridResizeProps) => {
const { ref } = useParams()
const state = useDatabaseSelectorStateSnapshot()
function onLayoutChange(layout: any) {
let updatedLayout = editableReport.layout
@@ -23,6 +45,8 @@ const GridResize = ({ startDate, endDate, interval, editableReport, setEditableR
setEditableReport(payload)
}
if (!editableReport) return null
return (
<>
<ReactGridLayout
@@ -47,13 +71,26 @@ const GridResize = ({ startDate, endDate, interval, editableReport, setEditableR
interval={interval}
attribute={x.attribute}
provider={x.provider}
label={x.label}
label={`${x.label}${ref !== state.selectedDatabaseId ? (x.provider === 'infra-monitoring' ? ' of replica' : ' on project') : ''}`}
customDateFormat={'MMM D, YYYY'}
/>
>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<Button
type="text"
icon={<X />}
className="ml-2 px-1"
onClick={() => onRemoveChart({ metric: { key: x.attribute } })}
/>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom">Remove chart</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
</ChartHandler>
<div className="absolute inset-x-0 top-3 ">
<div className="flex justify-around">
<div className="flex h-3 w-24 cursor-move flex-col space-y-2">
<div className="hidden h-3 w-full border-4 border-dotted border-green-900 opacity-50 transition-all hover:opacity-100 group-hover:block"></div>
<div className="hidden h-3 w-full border-4 border-dotted border-green-900 opacity-50 transition-all hover:opacity-100 group-hover:block" />
</div>
</div>
</div>
@@ -0,0 +1,57 @@
import { Home } from 'lucide-react'
import { useIsFeatureEnabled } from 'hooks'
import { METRICS, METRIC_CATEGORIES } from 'lib/constants/metrics'
import {
DropdownMenuCheckboxItem,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from 'ui'
interface MetricOptionsProps {
config: any
handleChartSelection: any
}
export const MetricOptions = ({ config, handleChartSelection }: MetricOptionsProps) => {
const { projectAuthAll: authEnabled, projectStorageAll: storageEnabled } = useIsFeatureEnabled([
'project_auth:all',
'project_storage:all',
])
const metricCategories = Object.values(METRIC_CATEGORIES).filter(({ key }) => {
if (key === 'api_auth') return authEnabled
if (key === 'api_storage') return storageEnabled
return true
})
return metricCategories.map((cat) => {
return (
<DropdownMenuSub key={cat.key}>
<DropdownMenuSubTrigger className="space-x-2">
{cat.icon ? cat.icon : <Home size={14} />}
<p>{cat.label}</p>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{METRICS.filter((metric) => metric?.category?.key === cat.key).map((metric) => {
return (
<DropdownMenuCheckboxItem
key={metric.key}
checked={config.layout?.some((x: any) => x.attribute === metric.key)}
onCheckedChange={(e) => handleChartSelection({ metric, value: e })}
>
<div className="flex flex-col space-y-0">
<span>{metric.label}</span>
</div>
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)
})
}
@@ -1,25 +1,23 @@
import { ChevronDown, Database, Plus, X } from 'lucide-react'
import { ComponentProps, useState } from 'react'
import SVG from 'react-inlinesvg'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import { Auth, Realtime, Storage } from 'icons'
import { BASE_PATH } from 'lib/constants'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
IconBox,
IconChevronDown,
IconCode,
IconDatabase,
IconKey,
IconPlus,
IconX,
IconZap,
Input,
Popover,
Select,
cn,
} from 'ui'
import DatePickers from '../Settings/Logs/Logs.DatePickers'
import { REPORTS_DATEPICKER_HELPERS } from './Reports.constants'
import type { ReportFilterItem } from './Reports.types'
@@ -41,7 +39,7 @@ const PRODUCT_FILTERS = [
filterValue: '/rest',
label: 'REST',
description: 'Requests made to PostgREST',
icon: IconDatabase,
icon: Database,
},
{
key: 'auth',
@@ -49,7 +47,7 @@ const PRODUCT_FILTERS = [
filterValue: '/auth',
label: 'Auth',
description: 'Auth and authorization requests',
icon: IconKey,
icon: Auth,
},
{
key: 'storage',
@@ -57,7 +55,7 @@ const PRODUCT_FILTERS = [
filterValue: '/storage',
label: 'Storage',
description: 'Storage asset requests',
icon: IconBox,
icon: Storage,
},
{
key: 'realtime',
@@ -65,7 +63,7 @@ const PRODUCT_FILTERS = [
filterValue: '/realtime',
label: 'Realtime',
description: 'Realtime connection requests',
icon: IconZap,
icon: Realtime,
},
{
key: 'graphql',
@@ -73,7 +71,7 @@ const PRODUCT_FILTERS = [
filterValue: '/graphql',
label: 'GraphQL',
description: 'Requests made to pg_graphql',
icon: IconCode,
icon: null,
},
]
@@ -86,6 +84,10 @@ const ReportFilterBar = ({
onRemoveFilters,
datepickerHelpers,
}: ReportFilterBarProps) => {
const { project } = useProjectContext()
// [Joshen] TODO: Once API support is out
const showReadReplicasUI = false // project?.is_read_replicas_enabled
const filterKeys = [
'request.path',
'request.method',
@@ -135,7 +137,7 @@ const ReportFilterBar = ({
}
return (
<div>
<div className="flex items-center justify-between">
<div className="flex flex-row justify-start items-center flex-wrap gap-2">
<DatePickers
onChange={onDatepickerChange}
@@ -148,7 +150,7 @@ const ReportFilterBar = ({
<Button
type="default"
className="inline-flex flex-row gap-2"
iconRight={<IconChevronDown size={14} />}
iconRight={<ChevronDown size={14} />}
>
<span>
{currentProductFilter === null ? 'All Requests' : currentProductFilter.label}
@@ -162,6 +164,7 @@ const ReportFilterBar = ({
<DropdownMenuSeparator />
{PRODUCT_FILTERS.map((productFilter) => {
const Icon = productFilter.icon
return (
<DropdownMenuItem
key={productFilter.key}
@@ -169,7 +172,17 @@ const ReportFilterBar = ({
disabled={productFilter.key === currentProductFilter?.key}
onClick={() => handleProductFilterChange(productFilter)}
>
<Icon size={20} className="mr-2" />
{productFilter.key === 'graphql' ? (
<SVG
src={`${BASE_PATH}/img/graphql.svg`}
className="w-[20px] h-[20px] mr-2"
preProcessor={(code) =>
code.replace(/svg/, 'svg class="m-auto text-color-inherit"')
}
/>
) : Icon !== null ? (
<Icon size={20} strokeWidth={1.5} className="mr-2" />
) : null}
<div className="flex flex-col">
<p
className={cn(
@@ -205,7 +218,7 @@ const ReportFilterBar = ({
size="tiny"
className="!p-0 !space-x-0"
onClick={() => onRemoveFilters([filter])}
icon={<IconX size="tiny" className="text-foreground-light" />}
icon={<X size={14} className="text-foreground-light" />}
>
<span className="sr-only">Remove</span>
</Button>
@@ -285,12 +298,13 @@ const ReportFilterBar = ({
asChild
type="default"
size="tiny"
icon={<IconPlus size="tiny" className={`text-foreground-light `} />}
icon={<Plus size={14} className={`text-foreground-light `} />}
>
<span>Add filter</span>
</Button>
</Popover>
</div>
{showReadReplicasUI && <DatabaseSelector />}
</div>
)
}
@@ -1,7 +1,10 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import Panel from 'components/ui/Panel'
import { ExternalLink, HelpCircle } from 'lucide-react'
import { NextRouter, useRouter } from 'next/router'
import { Button, IconExternalLink, IconHelpCircle, Loading } from 'ui'
import { useParams } from 'common'
import Panel from 'components/ui/Panel'
import { Button, Loading, cn } from 'ui'
import { LogsEndpointParams } from '../Settings/Logs'
import type { BaseReportParams, ReportQueryType } from './Reports.types'
@@ -27,15 +30,16 @@ export interface ReportWidgetRendererProps<T = any> extends ReportWidgetProps<T>
projectRef: string
}
const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
const ReportWidget = (props: ReportWidgetProps) => {
const router = useRouter()
const { ref } = router.query
const { ref } = useParams()
const projectRef = ref as string
return (
<Panel
noMargin
noHideOverflow
className={'pb-0 ' + props.className}
className={cn('pb-0', props.className)}
bodyClassName="h-full"
wrapWithLoading={false}
>
@@ -47,11 +51,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
{props?.tooltip && (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconHelpCircle
className="text-foreground-light"
size="tiny"
strokeWidth={1.5}
/>
<HelpCircle className="text-foreground-light" size={14} />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
@@ -76,7 +76,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
<Tooltip.Trigger asChild>
<Button
type="default"
icon={<IconExternalLink strokeWidth={1.5} />}
icon={<ExternalLink />}
className="px-1"
onClick={() => {
const isDbQueryType = props.queryType === 'db'
@@ -95,10 +95,7 @@ const ReportWidget: React.FC<ReportWidgetProps> = (props) => {
query.ite = props.params!.iso_timestamp_end
}
router.push({
pathname,
query,
})
router.push({ pathname, query })
}}
/>
</Tooltip.Trigger>
@@ -46,7 +46,6 @@ export const CreateReportModal = ({ visible, onCancel, afterSubmit }: CreateRepo
},
})
toast.success('New report created')
const newReportId = res[0].id
router.push(`/project/${ref}/reports/${newReportId}`)
afterSubmit()
@@ -1,39 +1,34 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import dayjs from 'dayjs'
import { groupBy, isNull } from 'lodash'
import { toJS } from 'mobx'
import { useEffect, useState } from 'react'
import {
Badge,
Button,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuPortal,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuSub,
DropdownMenuTrigger,
IconArrowRight,
IconHome,
IconPlus,
IconSave,
IconSettings,
} from 'ui'
import toast from 'react-hot-toast'
import { useParams } from 'common/hooks'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import { Loading } from 'components/ui/Loading'
import NoPermission from 'components/ui/NoPermission'
import { useCheckPermissions, useIsFeatureEnabled } from 'hooks'
import { METRICS, METRIC_CATEGORIES, TIME_PERIODS_REPORTS } from 'lib/constants/metrics'
import { useContentQuery } from 'data/content/content-query'
import { useContentUpdateMutation } from 'data/content/content-update-mutation'
import { useCheckPermissions } from 'hooks'
import { TIME_PERIODS_REPORTS } from 'lib/constants/metrics'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { useProjectContentStore } from 'stores/projectContentStore'
import { ArrowRight, Plus, Save, Settings } from 'lucide-react'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
Tooltip_Shadcn_,
} from 'ui'
import GridResize from './GridResize'
import { MetricOptions } from './MetricOptions'
import { LAYOUT_COLUMN_COUNT } from './Reports.constants'
import { useContentQuery } from 'data/content/content-query'
const DEFAULT_CHART_COLUMN_COUNT = 12
const DEFAULT_CHART_ROW_COUNT = 4
@@ -41,87 +36,46 @@ const DEFAULT_CHART_ROW_COUNT = 4
const Reports = () => {
const { id, ref } = useParams()
const { profile } = useProfile()
const { project } = useProjectContext()
const hasReadReplicasEnabled = project?.is_read_replicas_enabled
const { projectAuthAll: authEnabled, projectStorageAll: storageEnabled } = useIsFeatureEnabled([
'project_auth:all',
'project_storage:all',
])
const reportQuery = useContentQuery(ref)
const currentReport =
reportQuery.data && reportQuery.data?.content.find((report) => report.id === id)
const [report, setReport] = useState<any>()
const [loading, setLoading] = useState<any>(false)
const [config, setConfig] = useState<any>(null)
const [originalConfig, setOriginalConfig] = useState<any>(null)
const [hasEdits, setHasEdits] = useState<any>(false)
const [saving, setSaving] = useState<any>(false)
const [config, setConfig] = useState<any>(undefined)
const [startDate, setStartDate] = useState<any>(null)
const [endDate, setEndDate] = useState<any>(null)
const [hasEdits, setHasEdits] = useState<any>(false)
const { data: userContents, isLoading } = useContentQuery(ref)
const { mutateAsync: saveReport, isLoading: isSaving } = useContentUpdateMutation({
onSuccess: () => {
toast.success('Successfully saved report!')
},
})
const currentReport = userContents?.content.find((report) => report.id === id)
const contentStore = useProjectContentStore(ref)
const canReadReport = useCheckPermissions(PermissionAction.READ, 'user_content', {
resource: {
type: 'report',
visibility: report?.visibility,
owner_id: report?.owner_id,
visibility: currentReport?.visibility,
owner_id: currentReport?.owner_id,
},
subject: { id: profile?.id },
})
const canUpdateReport = useCheckPermissions(PermissionAction.UPDATE, 'user_content', {
resource: {
type: 'report',
visibility: report?.visibility,
owner_id: report?.owner_id,
visibility: currentReport?.visibility,
owner_id: currentReport?.owner_id,
},
subject: { id: profile?.id },
})
/*
* fetchReport()
*
* Fetches the report and sets the main states of the page
* Also sets an 'original' report for comaprison.
*
* toJS() is used to deepcopy mobx to js
*/
const fetchReport = async () => {
setLoading(true)
await contentStore.load()
const reportData = contentStore.byId(id)
if (reportData) {
setReport(reportData)
// [Joshen TODO] Worth refactoring - no need for so many states
setConfig(toJS(reportData.content))
setOriginalConfig(toJS(reportData.content))
}
setLoading(false)
return reportData
}
/*
* handleDateRangePicker()
*
* Sets date range of reports, using the DateRangePicker component
*/
function handleDateRangePicker({ period_start, period_end }: any) {
setStartDate(period_start.date)
setEndDate(period_end.date)
}
/*
* checkEditState()
*
* Makes a comparison of report changes vs the original report
*
* returns setHasEdits(bool)
*/
function checkEditState() {
if (config === undefined) return
/*
* Shallow copying the config state variable maintains a mobx reference
* Instead, we stringify it and parse it again to remove anything
@@ -131,7 +85,7 @@ const Reports = () => {
* want to compare fixed dates as possible differences from saved and edited versions of report.
*/
let _config = JSON.parse(JSON.stringify(config))
let _original = JSON.parse(JSON.stringify(originalConfig))
let _original = JSON.parse(JSON.stringify(currentReport?.content))
if (!_original || !_config) return
@@ -159,58 +113,23 @@ const Reports = () => {
setHasEdits(true)
}
}
// Reloads the entire report when id changes
useEffect(() => {
fetchReport()
}, [id])
// Runs when any changes are made to report
useEffect(() => {
checkEditState()
}, [config])
if (!config) {
return <Loading />
}
if (!canReadReport) {
return <NoPermission isFullPage resourceText="access this project's report" />
}
// Updates the report and reloads the report again
const saveReport = async () => {
setSaving(true)
const payload = {
content: config,
}
await contentStore.update(id, payload, 'report')
await fetchReport()
const onSaveReport = async () => {
if (ref === undefined) return console.error('Project ref is required')
if (id === undefined) return console.error('Report ID is required')
await saveReport({ projectRef: ref, id, type: 'report', content: config })
setHasEdits(false)
setSaving(false)
}
/*
* handleChartSelection()
*
* Handler for the chart behavior
*/
function handleChartSelection({ metric, value }: any) {
if (value) {
pushChart({ metric })
} else {
popChart({ metric })
}
if (value) pushChart({ metric })
else popChart({ metric })
}
/*
* pushChart()
*
* Adds a new chart to the report
*/
function pushChart({ metric }: any) {
let current = config.layout
const index = current.length + 1
const current = [...config.layout]
let x = 0
let y = null
@@ -257,14 +176,9 @@ const Reports = () => {
})
}
/*
* popChart()
*
* Removes a chart from the report based on its attribute
*/
function popChart({ metric }: any) {
const { key } = metric
let current = config.layout
const current = [...config.layout]
const foundIndex = current.findIndex((x: any, i: number) => {
if (x.attribute === key) {
@@ -278,51 +192,48 @@ const Reports = () => {
})
}
const metricCategories = Object.values(METRIC_CATEGORIES).filter(({ key }) => {
if (key === 'api_auth') return authEnabled
if (key === 'api_storage') return storageEnabled
return true
})
useEffect(() => {
if (currentReport !== undefined) setConfig(currentReport?.content)
}, [currentReport])
const MetricOptions = () => {
return (
<>
{metricCategories.map((cat) => {
return (
<DropdownMenuSub key={cat.key}>
<DropdownMenuSubTrigger className="space-x-2">
{cat.icon ? cat.icon : <IconHome size="tiny" />}
<p>{cat.label}</p>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{METRICS.filter((metric) => metric?.category?.key === cat.key).map((metric) => {
return (
<DropdownMenuCheckboxItem
key={metric.key}
checked={config.layout?.some((x: any) => x.attribute === metric.key)}
onCheckedChange={(e) => handleChartSelection({ metric, value: e })}
>
<div className="flex flex-col space-y-0">
<span>{metric.label}</span>
</div>
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)
})}
</>
)
useEffect(() => {
checkEditState()
}, [config])
if (isLoading) {
return <Loading />
}
if (!canReadReport) {
return <NoPermission isFullPage resourceText="access this project's report" />
}
return (
<div className="flex flex-col space-y-4" style={{ maxHeight: '100%' }}>
<div className="space-y-0.5">
<h1 className="text-xl text-foreground">{currentReport?.name || 'Reports'}</h1>
<p className="text-foreground-light">{currentReport?.description}</p>
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl text-foreground">{currentReport?.name || 'Reports'}</h1>
<p className="text-foreground-light">{currentReport?.description}</p>
</div>
{hasEdits && (
<div className="flex items-center gap-x-2">
<Button
type="default"
onClick={() => setConfig(currentReport?.content)}
disabled={isSaving}
>
Cancel
</Button>
<Button
type="primary"
icon={<Save />}
onClick={() => onSaveReport()}
loading={isSaving}
>
Save changes
</Button>
</div>
)}
</div>
<div className="mb-4 flex items-center justify-between space-x-3">
<div className="flex items-center space-x-3">
@@ -330,7 +241,7 @@ const Reports = () => {
onChange={handleDateRangePicker}
value="7d"
options={TIME_PERIODS_REPORTS}
loading={loading}
loading={isLoading}
/>
{startDate && endDate && (
@@ -339,7 +250,7 @@ const Reports = () => {
{dayjs(startDate).format('MMM D, YYYY')}
</span>
<span className="text-foreground-lighter">
<IconArrowRight size={12} />
<ArrowRight size={12} />
</span>
<span className="text-sm text-foreground-light">
{dayjs(endDate).format('MMM D, YYYY')}
@@ -348,75 +259,48 @@ const Reports = () => {
)}
</div>
<div className="flex items-center space-x-2">
{hasEdits && (
<div className="hidden xl:inline-block">
<Badge variant="brand">There are unsaved changes</Badge>
</div>
)}
{hasEdits && (
<Button
type={!hasEdits ? 'default' : 'primary'}
disabled={!hasEdits}
icon={<IconSave />}
onClick={() => saveReport()}
loading={saving}
>
{hasEdits && 'Save changes'}
</Button>
)}
<div className="flex items-center gap-x-2">
{canUpdateReport ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" iconRight={<IconSettings />}>
<Button type="default" iconRight={<Settings size={14} />}>
<span>Add / Remove charts</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
<MetricOptions />
<MetricOptions config={config} handleChartSelection={handleChartSelection} />
</DropdownMenuContent>
</DropdownMenu>
) : (
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger asChild>
<Button disabled type="default" iconRight={<IconSettings />}>
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<Button disabled type="default" iconRight={<Settings size={14} />}>
Add / Remove charts
</Button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-alternative py-1 px-2 leading-none shadow',
'border border-background',
].join(' ')}
>
<span className="text-xs text-foreground">
You need additional permissions to update this project's report
</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="bottom" className="w-56 text-center" align="end">
You need additional permissions to update this project's report
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
)}
{hasReadReplicasEnabled && <DatabaseSelector />}
</div>
</div>
{config.layout.length <= 0 ? (
{config?.layout !== undefined && config.layout.length <= 0 ? (
<div className="flex min-h-full items-center justify-center rounded border-2 border-dashed p-16 border-default">
{canUpdateReport ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" iconRight={<IconPlus />}>
<Button type="default" iconRight={<Plus size={14} />}>
<span>
{config.layout.length <= 0 ? 'Add your first chart' : 'Add another chart'}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="center">
<MetricOptions />
<MetricOptions config={config} handleChartSelection={handleChartSelection} />
</DropdownMenuContent>
</DropdownMenu>
) : (
@@ -431,6 +315,7 @@ const Reports = () => {
endDate={endDate}
interval={config.interval}
editableReport={config}
onRemoveChart={popChart}
setEditableReport={setConfig}
/>
)}
@@ -1,3 +1,8 @@
import sumBy from 'lodash/sumBy'
import { Fragment, useState } from 'react'
import { ChevronRight } from 'lucide-react'
import { useParams } from 'common'
import {
jsonSyntaxHighlight,
TextFormatter,
@@ -5,12 +10,9 @@ import {
import Table from 'components/to-be-cleaned/Table'
import BarChart from 'components/ui/Charts/BarChart'
import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted'
import sumBy from 'lodash/sumBy'
import { Fragment, useState } from 'react'
import { Button, Collapsible, IconChevronRight } from 'ui'
import { Button, Collapsible } from 'ui'
import { queryParamsToObject } from '../Reports.utils'
import { ReportWidgetProps, ReportWidgetRendererProps } from '../ReportWidget'
import { useParams } from 'common/hooks'
export const NetworkTrafficRenderer = (
props: ReportWidgetProps<{
@@ -63,6 +65,7 @@ export const NetworkTrafficRenderer = (
</div>
)
}
export const TotalRequestsChartRenderer = (
props: ReportWidgetProps<{
timestamp: string
@@ -107,15 +110,17 @@ export const TopApiRoutesRenderer = (
}>
) => {
const { ref: projectRef } = useParams()
const [showMore, setShowMore] = useState(false)
const headerClasses = '!text-xs !py-2 p-0 font-bold !bg-surface-200 !border-x-0 !rounded-none'
const cellClasses = '!text-xs !py-2 !border-x-0 !rounded-none align-middle'
if (props.data.length === 0) return null
const headerClasses = '!text-xs !py-2 p-0 font-bold !bg-surface-200'
const cellClasses = '!text-xs !py-2'
return (
<Collapsible>
<Table
className="rounded-t-none"
head={
<>
<Table.th className={headerClasses}>Request</Table.th>
@@ -128,10 +133,10 @@ export const TopApiRoutesRenderer = (
body={
<>
{props.data.map((datum, index) => (
<Fragment key={datum.path + (datum.search || '')}>
<Fragment key={datum.method + datum.path + (datum.search || '')}>
<Table.tr
className={[
'p-0 transition transform duration-700',
'p-0 transition transform cursor-pointer hover:bg-surface-200',
showMore && index >= 3 ? 'w-full h-full opacity-100' : '',
!showMore && index >= 3 ? ' w-0 h-0 translate-y-10 opacity-0' : '',
].join(' ')}
@@ -270,10 +275,10 @@ interface RouteTdContentProps {
const RouteTdContent = (datum: RouteTdContentProps) => (
<Collapsible>
<Collapsible.Trigger asChild>
<div className="flex gap-2">
<div className="flex gap-2 items-center">
<Button asChild type="text" className=" !py-0 !p-1" title="Show more route details">
<span>
<IconChevronRight
<ChevronRight
size={14}
className="transition data-open-parent:rotate-90 data-closed-parent:rotate-0"
/>
@@ -1,29 +1,23 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import * as z from 'zod'
import type {
EnvironmentTargets,
Integration,
IntegrationProjectConnection,
} from 'data/integrations/integrations.types'
import { useVercelConnectionUpdateMutation } from 'data/integrations/vercel-connection-update-mutate'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
FormControl_Shadcn_,
FormDescription_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
FormLabel_Shadcn_,
Form_Shadcn_,
IconClock,
Switch,
cn,
} from 'ui'
import * as z from 'zod'
import { ScaffoldDivider } from 'components/layouts/Scaffold'
import type {
Integration,
IntegrationProjectConnection,
} from 'data/integrations/integrations.types'
import { useVercelConnectionUpdateMutation } from 'data/integrations/vercel-connection-update-mutate'
import { useFlag } from 'hooks'
const VercelIntegrationConnectionForm = ({
connection,
@@ -32,190 +26,124 @@ const VercelIntegrationConnectionForm = ({
connection: IntegrationProjectConnection
integration: Integration
}) => {
const config = connection.metadata.supabaseConfig
const enableVercelConnectionsConfig = useFlag('enableVercelConnectionsConfig')
const envSyncTargets = connection.env_sync_targets ?? []
const FormSchema = z.object({
environmentVariablesProduction: z
.boolean()
.default(config?.environmentVariables?.production ?? true),
authRedirectUrisProduction: z.boolean().default(config?.authRedirectUris?.production ?? true),
environmentVariablesPreview: z.boolean().default(config?.environmentVariables?.preview ?? true),
authRedirectUrisPreview: z.boolean().default(config?.authRedirectUris?.preview ?? true),
environmentVariablesProduction: z.boolean().default(envSyncTargets.includes('production')),
environmentVariablesPreview: z.boolean().default(envSyncTargets.includes('preview')),
environmentVariablesDevelopment: z.boolean().default(envSyncTargets.includes('development')),
})
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
environmentVariablesProduction: config?.environmentVariables?.production ?? true,
environmentVariablesPreview: config?.environmentVariables?.preview ?? true,
authRedirectUrisProduction: config?.authRedirectUris?.production ?? true,
authRedirectUrisPreview: config?.authRedirectUris?.preview ?? true,
environmentVariablesProduction: envSyncTargets.includes('production'),
environmentVariablesPreview: envSyncTargets.includes('preview'),
environmentVariablesDevelopment: envSyncTargets.includes('development'),
},
})
const { mutate: updateVercelConnection } = useVercelConnectionUpdateMutation({
onSuccess: (data) => {
toast.success(`Updated Supabase directory`)
},
onSuccess: () => toast.success(`Updated Supabase directory`),
})
function onSubmit(data: z.infer<typeof FormSchema>) {
/**
* remove this hardcoded if statement when we are ready to enable this feature
*/
if (!enableVercelConnectionsConfig) return
const {
environmentVariablesProduction,
environmentVariablesPreview,
environmentVariablesDevelopment,
} = data
const metadata = {
...connection.metadata,
}
const envSyncTargets: string[] = []
metadata.supabaseConfig = {
environmentVariables: {
production: data.environmentVariablesProduction,
preview: data.environmentVariablesPreview,
},
authRedirectUris: {
production: data.authRedirectUrisProduction,
preview: data.authRedirectUrisPreview,
},
}
if (environmentVariablesProduction) envSyncTargets.push('production')
if (environmentVariablesPreview) envSyncTargets.push('preview')
if (environmentVariablesDevelopment) envSyncTargets.push('development')
updateVercelConnection({
id: connection.id,
metadata,
envSyncTargets: envSyncTargets as EnvironmentTargets[],
organizationIntegrationId: integration.id,
})
}
return (
<Form_Shadcn_ {...form}>
<div className="py-4 px-8">
<Alert_Shadcn_ variant="default">
<IconClock className="h-4 w-4" strokeWidth={2} />
<AlertTitle_Shadcn_>Vercel Connection configuration coming soon</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
This configuration will allow you to control the environment variables and auth
redirects for production and preview deployments.
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</div>
<ScaffoldDivider />
<form
onSubmit={form.handleSubmit(onSubmit)}
className={cn(!enableVercelConnectionsConfig && 'opacity-30', 'w-full space-y-6')}
>
<div>
{/* {isUpdatingVercelConnection && 'isUpdatingVercelConnection'} */}
<div className="flex flex-col gap-6 px-8 py-8">
<h5 className="text-foreground text-sm">Vercel Production deployments </h5>
<form onSubmit={form.handleSubmit(onSubmit)} className={'w-full space-y-6'}>
<div className="px-6 py-4 flex flex-col gap-y-4">
<h5 className="text-foreground text-sm">
Sync environment variables for selected target environments
</h5>
<div className="flex flex-col gap-4">
<FormField_Shadcn_
control={form.control}
name="environmentVariablesProduction"
render={({ field }) => (
<FormItem_Shadcn_ className="flex flex-row items-center justify-between">
<div>
<FormLabel_Shadcn_ className="!text">
Sync environment variables for Vercel Production deployments
</FormLabel_Shadcn_>
<FormDescription_Shadcn_ className="text-xs text-foreground-lighter">
Deploy Edge Functions when merged into Production Branch
</FormDescription_Shadcn_>
</div>
<FormItem_Shadcn_ className="space-y-0 flex gap-x-4">
<FormControl_Shadcn_>
<Switch
className="mt-1"
checked={field.value}
disabled={!enableVercelConnectionsConfig}
onCheckedChange={(e) => {
field.onChange(e)
form.handleSubmit(onSubmit)()
}}
/>
</FormControl_Shadcn_>
</FormItem_Shadcn_>
)}
/>
{/* <Button htmlType="submit">Submit</Button> */}
<FormField_Shadcn_
control={form.control}
name="authRedirectUrisProduction"
render={({ field }) => (
<FormItem_Shadcn_ className="flex flex-row items-center justify-between">
<div>
<FormLabel_Shadcn_ className="!text">
Auto update Auth Redirect URIs for Vercel Production Deployments
</FormLabel_Shadcn_>
<FormLabel_Shadcn_ className="!text">Production</FormLabel_Shadcn_>
<FormDescription_Shadcn_ className="text-xs text-foreground-lighter">
Deploy Edge Functions when merged into Production Branch
Sync environment variables for <code>production</code> environment.
</FormDescription_Shadcn_>
</div>
<FormControl_Shadcn_>
<Switch
checked={field.value}
disabled={!enableVercelConnectionsConfig}
onCheckedChange={(e) => {
field.onChange(e)
form.handleSubmit(onSubmit)()
}}
/>
</FormControl_Shadcn_>
</FormItem_Shadcn_>
)}
/>
</div>
<ScaffoldDivider />
<div className="flex flex-col gap-6 px-8 py-8">
<h5 className="text-foreground text-sm">Vercel Preview deployments </h5>
<FormField_Shadcn_
control={form.control}
name="environmentVariablesPreview"
render={({ field }) => (
<FormItem_Shadcn_ className="flex flex-row items-center justify-between">
<div>
<FormLabel_Shadcn_ className="!text">
Sync environment variables for Vercel Preview Deployments
</FormLabel_Shadcn_>
<FormDescription_Shadcn_ className="text-xs text-foreground-lighter">
Preview deployments will be able to connect to Supabase Database Preview
branches
</FormDescription_Shadcn_>
</div>
<FormItem_Shadcn_ className="space-y-0 flex gap-x-4">
<FormControl_Shadcn_>
<Switch
className="mt-1"
checked={field.value}
disabled={!enableVercelConnectionsConfig}
onCheckedChange={(e) => {
field.onChange(e)
form.handleSubmit(onSubmit)()
}}
/>
</FormControl_Shadcn_>
<div>
<FormLabel_Shadcn_ className="!text">Preview</FormLabel_Shadcn_>
<FormDescription_Shadcn_ className="text-xs text-foreground-lighter">
Sync environment variables for <code>preview</code> environment.
</FormDescription_Shadcn_>
</div>
</FormItem_Shadcn_>
)}
/>
<FormField_Shadcn_
control={form.control}
name="authRedirectUrisPreview"
name="environmentVariablesDevelopment"
render={({ field }) => (
<FormItem_Shadcn_ className="flex flex-row items-center justify-between">
<div>
<FormLabel_Shadcn_ className="!text">
Auto update Auth Redirect URIs for Vercel Preview Deployments
</FormLabel_Shadcn_>
<FormDescription_Shadcn_ className="text-xs text-foreground-lighter">
Deploy Edge Functions when merged into Production Branch
</FormDescription_Shadcn_>
</div>
<FormItem_Shadcn_ className="space-y-0 flex gap-x-4">
<FormControl_Shadcn_>
<Switch
className="mt-1"
checked={field.value}
disabled={!enableVercelConnectionsConfig}
onCheckedChange={(e) => {
field.onChange(e)
form.handleSubmit(onSubmit)()
}}
/>
</FormControl_Shadcn_>
<div>
<FormLabel_Shadcn_ className="!text">Development</FormLabel_Shadcn_>
<FormDescription_Shadcn_ className="text-xs text-foreground-lighter">
Sync environment variables for <code>development</code> environment.
</FormDescription_Shadcn_>
</div>
</FormItem_Shadcn_>
)}
/>
@@ -9,7 +9,7 @@ interface ReportsLayoutProps {
const ReportsLayout = ({ title, children }: PropsWithChildren<ReportsLayoutProps>) => {
return (
<ProjectLayout title={title} product="Reports" productMenu={<ReportsMenu />}>
<ProjectLayout title={title} product="Reports" productMenu={<ReportsMenu />} isBlocking={false}>
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
{children}
</main>
@@ -1,19 +1,26 @@
import type { Dictionary } from 'types'
import { isUndefined } from 'lodash'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect, useState } from 'react'
import { Button, IconActivity, IconAlertCircle, IconBarChart, IconLoader } from 'ui'
import { PropsWithChildren, useState } from 'react'
import { Button } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import AreaChart from 'components/ui/Charts/AreaChart'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { AnalyticsInterval } from 'data/analytics/constants'
import {
InfraMonitoringAttribute,
useInfraMonitoringQuery,
} from 'data/analytics/infra-monitoring-query'
import { useProjectDailyStatsQuery } from 'data/analytics/project-daily-stats-query'
import { Activity, BarChartIcon, Loader2 } from 'lucide-react'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { WarningIcon } from 'ui-patterns/Icons/StatusIcons'
import type { ChartData } from './ChartHandler.types'
import { BarChart } from './ChartRenderer'
interface ChartHandlerProps {
label: string
attribute: string
provider: string
provider: 'infra-monitoring' | 'daily-stats'
startDate: string
endDate: string
interval: string
@@ -56,72 +63,77 @@ const ChartHandler = ({
const router = useRouter()
const { ref } = router.query
// internal state is overridden by isLoading prop
const [fetching, setFetching] = useState<boolean>(false)
const [fetchedData, setFetchedData] = useState<any>(undefined)
const { project } = useProjectContext()
const isReadReplicasEnabled = project?.is_read_replicas_enabled
const state = useDatabaseSelectorStateSnapshot()
const [chartStyle, setChartStyle] = useState<string>(defaultChartStyle)
const chartData = data || fetchedData
const loading = isLoading || fetching
useEffect(() => {
let cancel = false
const databaseIdentifier = isReadReplicasEnabled ? state.selectedDatabaseId : undefined
const fetchChartData = async () => {
setFetching(true)
const { data: dailyStatsData, isLoading: isFetchingDailyStats } = useProjectDailyStatsQuery(
{
projectRef: ref as string,
attribute: attribute as InfraMonitoringAttribute,
startDate,
endDate,
interval: interval as AnalyticsInterval,
databaseIdentifier,
},
{ enabled: provider === 'daily-stats' && data === undefined }
)
const url = `${API_URL}/projects/${ref}/${provider}?attribute=${attribute}&startDate=${encodeURIComponent(
startDate
)}&endDate=${encodeURIComponent(endDate)}&interval=${interval ? interval : '1d'}`
const { data: infraMonitoringData, isLoading: isFetchingInfraMonitoring } =
useInfraMonitoringQuery(
{
projectRef: ref as string,
attribute: attribute as InfraMonitoringAttribute,
startDate,
endDate,
interval: interval as AnalyticsInterval,
databaseIdentifier,
},
{ enabled: provider === 'infra-monitoring' && data === undefined }
)
const { error, ...res } = await get(url)
if ((error || isUndefined(res)) && !cancel) {
setFetching(false)
setFetchedData(undefined)
return console.error('Chart error:', error)
}
// Convert null values to 0
// TODO: Chart endpoint should handle this data formatting to 0 instead of client.
const formattedChartData = (res?.data ?? []).map(
(dataPoint: Dictionary<any>, idx: number) => {
return {
...dataPoint,
[attribute]: dataPoint[attribute] ? Number(dataPoint[attribute]) : 0,
}
}
)
if (!cancel) {
setFetchedData({ ...res, data: formattedChartData })
setFetching(false)
}
}
// only fetch if data prop is not provided
if (data === undefined && !isLoading) {
fetchChartData()
}
return () => {
cancel = true
}
}, [startDate])
highlightedValue = highlightedValue
? highlightedValue
: provider === 'daily-stats' && !attribute.includes('ingress') && !attribute.includes('egress')
? chartData?.maximum
const chartData =
data ||
(provider === 'infra-monitoring'
? infraMonitoringData
: provider === 'daily-stats'
? chartData?.total
: provider === 'log-stats'
? chartData?.totalGrouped?.[attribute]
: chartData?.data[chartData?.data.length - 1]?.[attribute]
? dailyStatsData
: undefined)
const loading =
isLoading ||
(provider === 'infra-monitoring'
? isFetchingInfraMonitoring
: provider === 'daily-stats'
? isFetchingDailyStats
: isLoading)
const shouldHighlightMaxValue =
provider === 'daily-stats' &&
!attribute.includes('ingress') &&
!attribute.includes('egress') &&
chartData !== undefined &&
'maximum' in chartData
const shouldHighlightTotalGroupedValue = chartData !== undefined && 'totalGrouped' in chartData
const _highlightedValue =
highlightedValue !== undefined
? highlightedValue
: shouldHighlightMaxValue
? chartData?.maximum
: provider === 'daily-stats'
? chartData?.total
: shouldHighlightTotalGroupedValue
? chartData?.totalGrouped?.[attribute]
: (chartData?.data[chartData?.data.length - 1] as any)?.[attribute as any]
if (loading) {
return (
<div className="flex h-52 w-full flex-col items-center justify-center space-y-4">
<IconLoader className="animate-spin text-border-strong" />
<div className="flex h-52 w-full flex-col items-center justify-center gap-y-2">
<Loader2 size={18} className="animate-spin text-border-strong" />
<p className="text-xs text-foreground-lighter">Loading data for {label}</p>
</div>
)
@@ -129,8 +141,8 @@ const ChartHandler = ({
if (isUndefined(chartData)) {
return (
<div className="flex h-52 w-full flex-col items-center justify-center space-y-4">
<IconAlertCircle className="text-border-strong" />
<div className="flex h-52 w-full flex-col items-center justify-center gap-y-2">
<WarningIcon />
<p className="text-xs text-foreground-lighter">Unable to load data for {label}</p>
</div>
)
@@ -139,37 +151,39 @@ const ChartHandler = ({
return (
<div className="h-full w-full">
<div className="absolute right-6 z-50 flex justify-between">
<div className="space-y-3">{children}</div>
{!hideChartType && (
<div>
<div className="flex w-full space-x-3">
<Button
type="default"
icon={chartStyle === 'bar' ? <IconActivity /> : <IconBarChart />}
className="px-1.5"
icon={chartStyle === 'bar' ? <Activity /> : <BarChartIcon />}
onClick={() => setChartStyle(chartStyle === 'bar' ? 'line' : 'bar')}
/>
</div>
</div>
)}
{children}
</div>
{chartStyle === 'bar' ? (
<BarChart
data={chartData?.data ?? []}
attribute={attribute}
yAxisLimit={chartData?.yAxisLimit}
format={format || chartData?.format}
highlightedValue={highlightedValue}
highlightedValue={_highlightedValue}
label={label}
customDateFormat={customDateFormat}
onBarClick={onBarClick}
/>
) : (
<AreaChart
data={chartData?.data ?? []}
data={(chartData?.data ?? []) as any}
format={format || chartData?.format}
xAxisKey="period_start"
yAxisKey={attribute}
highlightedValue={highlightedValue}
highlightedValue={_highlightedValue}
title={label}
customDateFormat={customDateFormat}
/>
@@ -1,95 +0,0 @@
import { toJS } from 'mobx'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import { useProjectContentStore } from 'stores/projectContentStore'
import { Input, Modal } from 'ui'
const EditModal = () => {
const router = useRouter()
const { id, ref } = router.query
const [visible, setVisible] = useState<boolean>(false)
const [loading, setLoading] = useState<boolean>(false)
const [name, setName] = useState<string>()
const [description, setDescription] = useState<string>()
const contentStore = useProjectContentStore(ref)
/*
* fetchReport()
*
* Fetches the report and sets the Name and description
*
* toJS() is used to deepcopy mobx to js
*/
const fetchReport = async () => {
setLoading(true)
await contentStore.load()
let reportData = contentStore.byId(id)
setName(toJS(reportData?.name))
setDescription(toJS(reportData?.description))
setLoading(false)
return reportData
}
useEffect(() => {
fetchReport()
setVisible(false)
}, [])
async function handleSubmit() {
try {
setLoading(true)
const { data, error } = await contentStore.update(id, {
name,
description,
})
if (error) throw error
setLoading(false)
await contentStore.load()
setVisible(!visible)
return data
} catch (error: any) {
toast.error(error.message)
setLoading(false)
}
}
return (
<Modal
title="Edit report"
visible={visible}
onCancel={() => setVisible(false)}
size="medium"
onConfirm={handleSubmit}
loading={loading}
>
<div className="w-full space-y-4">
<Input
layout="horizontal"
label="Name"
descriptionText="Name cannot be longer than 128 char"
value={name}
onChange={(e) => setName(e.target.value)}
>
Name
</Input>
<Input
layout="horizontal"
label="Description"
descriptionText="description cannot be longer than 256 char"
value={description}
onChange={(e) => setDescription(e.target.value)}
>
Description
</Input>
</div>
</Modal>
)
}
export default observer(EditModal)
@@ -1,4 +1,4 @@
import { IconBarChart2 } from 'ui'
import { BarChart2 } from 'lucide-react'
interface EmptyStateProps {
title?: string
@@ -17,7 +17,7 @@ const EmptyState = ({
border-dashed border-control py-4 text-center
"
>
<IconBarChart2 className="text-foreground-light" />
<BarChart2 size={20} className="text-foreground-light" />
<div>
<p className="text-xs text-foreground-light">{title}</p>
<p className="text-xs text-foreground-light">{message}</p>
@@ -1,4 +1,4 @@
import { IconBarChart2 } from 'ui'
import { BarChart2 } from 'lucide-react'
import { useChartSize } from './Charts.utils'
interface Props {
@@ -23,7 +23,7 @@ const NoDataPlaceholder: React.FC<Props> = ({
// extra 20 px for the x ticks
style={{ minHeight: minHeight + 20 }}
>
<IconBarChart2 className="text-border-stronger" />
<BarChart2 size={20} className="text-border-stronger" />
<div>
<p className="text-foreground-light text-xs">{title}</p>
{message && <p className="text-foreground-lighter text-xs">{message}</p>}
+2
View File
@@ -15,3 +15,5 @@ export interface AnalyticsData {
yAxisLimit: number
hasNoData?: boolean
}
export type AnalyticsInterval = '1m' | '5m' | '10m' | '30m' | '1h' | '1d'
@@ -1,28 +1,38 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import type { AnalyticsData } from './constants'
import { get, handleError } from 'data/fetchers'
import type { AnalyticsData, AnalyticsInterval } from './constants'
import { analyticsKeys } from './keys'
export type InfraMonitoringAttribute =
| 'max_cpu_usage'
| 'avg_cpu_usage'
| 'disk_io_budget'
| 'ram_usage'
| 'disk_io_consumption'
| 'swap_usage'
export type InfraMonitoringVariables = {
projectRef?: string
attribute:
| 'max_cpu_usage'
| 'avg_cpu_usage'
| 'disk_io_budget'
| 'ram_usage'
| 'disk_io_consumption'
| 'swap_usage'
attribute: InfraMonitoringAttribute
startDate?: string
endDate?: string
interval?: '1m' | '5m' | '10m' | '30m' | '1h' | '1d'
interval?: AnalyticsInterval
dateFormat?: string
databaseIdentifier?: string
modifier?: (x: number) => number
}
export async function getInfraMonitoring(
{ projectRef, attribute, startDate, endDate, interval = '1d' }: InfraMonitoringVariables,
{
projectRef,
attribute,
startDate,
endDate,
interval = '1d',
databaseIdentifier,
}: InfraMonitoringVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('Project ref is required')
@@ -30,15 +40,22 @@ export async function getInfraMonitoring(
if (!startDate) throw new Error('Start date is required')
if (!endDate) throw new Error('End date is required')
const data = await get(
`${API_URL}/projects/${projectRef}/infra-monitoring?attribute=${attribute}&startDate=${encodeURIComponent(
startDate
)}&endDate=${encodeURIComponent(endDate)}&interval=${interval}`,
{ signal }
)
const { data, error } = await get('/platform/projects/{ref}/infra-monitoring', {
params: {
path: { ref: projectRef },
query: {
attribute,
startDate,
endDate,
interval,
databaseIdentifier,
},
},
signal,
})
if (data.error) throw data.error
return data as AnalyticsData
if (error) throw handleError(error)
return data as unknown as AnalyticsData
}
export type InfraMonitoringData = Awaited<ReturnType<typeof getInfraMonitoring>>
@@ -52,6 +69,7 @@ export const useInfraMonitoringQuery = <TData = InfraMonitoringData>(
endDate,
interval = '1d',
dateFormat = 'DD MMM',
databaseIdentifier,
modifier,
}: InfraMonitoringVariables,
{
@@ -60,9 +78,18 @@ export const useInfraMonitoringQuery = <TData = InfraMonitoringData>(
}: UseQueryOptions<InfraMonitoringData, InfraMonitoringError, TData> = {}
) =>
useQuery<InfraMonitoringData, InfraMonitoringError, TData>(
analyticsKeys.infraMonitoring(projectRef, { attribute, startDate, endDate, interval }),
analyticsKeys.infraMonitoring(projectRef, {
attribute,
startDate,
endDate,
interval,
databaseIdentifier,
}),
({ signal }) =>
getInfraMonitoring({ projectRef, attribute, startDate, endDate, interval }, signal),
getInfraMonitoring(
{ projectRef, attribute, startDate, endDate, interval, databaseIdentifier },
signal
),
{
enabled:
enabled &&
+3 -1
View File
@@ -115,18 +115,20 @@ export const analyticsKeys = {
startDate,
endDate,
interval,
databaseIdentifier,
}: {
attribute?: string
startDate?: string
endDate?: string
interval?: string
databaseIdentifier?: string
}
) =>
[
'projects',
projectRef,
'infra-monitoring',
{ attribute, startDate, endDate, interval },
{ attribute, startDate, endDate, interval, databaseIdentifier },
] as const,
usageApiCounts: (projectRef: string | undefined, interval: string | undefined) =>
['projects', projectRef, 'usage.api-counts', interval] as const,
@@ -0,0 +1,119 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { get, handleError } from 'data/fetchers'
import type { AnalyticsData } from './constants'
import { analyticsKeys } from './keys'
export type ProjectDailyStatsAttribute =
| 'max_cpu_usage'
| 'avg_cpu_usage'
| 'disk_io_budget'
| 'ram_usage'
| 'disk_io_consumption'
| 'swap_usage'
export type ProjectDailyStatsInterval = '1m' | '5m' | '10m' | '30m' | '1h' | '1d'
export type ProjectDailyStatsVariables = {
projectRef?: string
attribute: ProjectDailyStatsAttribute
startDate?: string
endDate?: string
interval?: ProjectDailyStatsInterval
dateFormat?: string
databaseIdentifier?: string
modifier?: (x: number) => number
}
export async function getProjectDailyStats(
{
projectRef,
attribute,
startDate,
endDate,
interval = '1d',
databaseIdentifier,
}: ProjectDailyStatsVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('Project ref is required')
if (!attribute) throw new Error('Attribute is required')
if (!startDate) throw new Error('Start date is required')
if (!endDate) throw new Error('End date is required')
const { data, error } = await get('/platform/projects/{ref}/daily-stats', {
params: {
path: { ref: projectRef },
query: {
attribute,
startDate,
endDate,
interval,
// [Joshen] TODO: Once API support is ready
// databaseIdentifier,
},
},
signal,
})
if (error) throw handleError(error)
return data as unknown as AnalyticsData
}
export type ProjectDailyStatsData = Awaited<ReturnType<typeof getProjectDailyStats>>
export type ProjectDailyStatsError = unknown
export const useProjectDailyStatsQuery = <TData = ProjectDailyStatsData>(
{
projectRef,
attribute,
startDate,
endDate,
interval = '1d',
dateFormat = 'DD MMM',
databaseIdentifier,
modifier,
}: ProjectDailyStatsVariables,
{
enabled = true,
...options
}: UseQueryOptions<ProjectDailyStatsData, ProjectDailyStatsError, TData> = {}
) =>
useQuery<ProjectDailyStatsData, ProjectDailyStatsError, TData>(
analyticsKeys.infraMonitoring(projectRef, {
attribute,
startDate,
endDate,
interval,
databaseIdentifier,
}),
({ signal }) =>
getProjectDailyStats(
{ projectRef, attribute, startDate, endDate, interval, databaseIdentifier },
signal
),
{
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof attribute !== 'undefined' &&
typeof startDate !== 'undefined' &&
typeof endDate !== 'undefined',
select(data) {
return {
...data,
data: data.data.map((x) => {
return {
...x,
[attribute]:
modifier !== undefined ? modifier(Number(x[attribute])) : Number(x[attribute]),
periodStartFormatted: dayjs(x.period_start).format(dateFormat),
}
}),
} as TData
},
staleTime: 1000 * 60, // default good for a minute
...options,
}
)
@@ -7,7 +7,7 @@ import type { ResponseError } from 'types'
import type { Content } from './content-query'
import { contentKeys } from './keys'
export type InsertContentPayload = Omit<components['schemas']['CreateContentParams'], 'content'> & {
export type InsertContentPayload = Omit<components['schemas']['CreateContentBody'], 'content'> & {
content: Content['content']
}
@@ -16,6 +16,8 @@ export type InsertContentVariables = {
payload: InsertContentPayload
}
export type InsertContentResponse = components['schemas']['UserContentObject']
export async function insertContent(
{ projectRef, payload }: InsertContentVariables,
signal?: AbortSignal
@@ -35,7 +37,8 @@ export async function insertContent(
})
if (error) throw error
return data
// [Joshen] There's an issue with the API codegen due to content endpoint having 2 versions
return data as unknown as InsertContentResponse[]
}
export type InsertContentData = Awaited<ReturnType<typeof insertContent>>
-12
View File
@@ -72,15 +72,3 @@ export const useContentQuery = <TData = ContentData>(
({ signal }) => getContent(projectRef, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
export const useContentPrefetch = (projectRef: string | undefined) => {
const client = useQueryClient()
return useCallback(() => {
if (projectRef) {
client.prefetchQuery(contentKeys.list(projectRef), ({ signal }) =>
getContent(projectRef, signal)
)
}
}, [projectRef])
}
@@ -0,0 +1,58 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import toast from 'react-hot-toast'
import { patch } from 'data/fetchers'
import type { ResponseError } from 'types'
import { contentKeys } from './keys'
type UpdateContentVariables = { projectRef: string; id: string; type: 'report'; content: any }
export async function updateContent(
{ projectRef, id, type, content }: UpdateContentVariables,
signal?: AbortSignal
) {
const { data, error } = await patch('/platform/projects/{ref}/content', {
params: {
// @ts-ignore API codegen issue
path: { ref: projectRef },
query: { id },
},
body: { id, type, content },
signal,
})
if (error) throw error
return data
}
type UpdateContentData = Awaited<ReturnType<typeof updateContent>>
export const useContentUpdateMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<UpdateContentData, ResponseError, UpdateContentVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<UpdateContentData, ResponseError, UpdateContentVariables>(
(args) => updateContent(args),
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(contentKeys.list(projectRef))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to update content: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
@@ -7,7 +7,7 @@ import type { ResponseError } from 'types'
import type { Content } from './content-query'
import { contentKeys } from './keys'
export type UpsertContentPayload = Omit<components['schemas']['UpsertContentParams'], 'content'> & {
export type UpsertContentPayload = Omit<components['schemas']['UpsertContentBody'], 'content'> & {
content: Content['content']
}
@@ -32,8 +32,7 @@ export async function getEnumeratedTypes(
})
if (error) handleError(error)
const enumeratedTypes = data.filter((type) => type.enums.length > 0)
return enumeratedTypes
return data
}
export type EnumeratedTypesData = Awaited<ReturnType<typeof getEnumeratedTypes>>
@@ -154,6 +154,7 @@ export type IntegrationProjectConnection = {
supabase_project_ref: string
foreign_project_id: string
organization_integration_id: string
env_sync_targets?: string[]
metadata: Imetadata
}
@@ -264,8 +265,10 @@ export type GitHubConnectionCreateVariables = {
connection: components['schemas']['CreateGitHubConnectionsBody']
}
export type EnvironmentTargets = 'production' | 'preview' | 'development'
export type UpdateConnectionPayload = {
id: string
organizationIntegrationId: string
metadata: Imetadata
envSyncTargets: EnvironmentTargets[]
}
@@ -6,18 +6,13 @@ import type { ResponseError } from 'types'
import type { UpdateConnectionPayload } from './integrations.types'
import { integrationKeys } from './keys'
export async function updateVercelConnection({
id,
metadata,
organizationIntegrationId,
}: UpdateConnectionPayload) {
export async function updateVercelConnection({ id, envSyncTargets }: UpdateConnectionPayload) {
const { data, error } = await patch('/platform/integrations/vercel/connections/{connection_id}', {
params: {
path: { connection_id: id },
},
body: {
// @ts-expect-error
metadata,
env_sync_targets: envSyncTargets,
},
})
+1 -1
View File
@@ -36,8 +36,8 @@ const useDbQuery = ({
orderBy?: string
}): DbQueryHook => {
const { project } = useProjectContext()
const state = useDatabaseSelectorStateSnapshot()
const { data: databases } = useReadReplicasQuery({ projectRef: project?.ref })
const connectionString = project?.is_read_replicas_enabled
? (databases || []).find((db) => db.identifier === state.selectedDatabaseId)?.connectionString
+12 -4
View File
@@ -1,17 +1,21 @@
import { useQuery } from '@tanstack/react-query'
import { Dispatch, SetStateAction, useState } from 'react'
import {
EXPLORER_DATEPICKER_HELPERS,
genQueryParams,
getDefaultHelper,
} from 'components/interfaces/Settings/Logs'
import { Dispatch, SetStateAction, useState } from 'react'
import type {
LogsEndpointParams,
Logs,
LogData,
Logs,
LogsEndpointParams,
} from 'components/interfaces/Settings/Logs/Logs.types'
import { API_URL } from 'lib/constants'
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
import { get, isResponseOk } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
export interface LogsQueryHook {
params: LogsEndpointParams
isLoading: boolean
@@ -27,6 +31,10 @@ const useLogsQuery = (
projectRef: string,
initialParams: Partial<LogsEndpointParams> = {}
): LogsQueryHook => {
// [Joshen] TODO once API is unblocked
const state = useDatabaseSelectorStateSnapshot()
const { data: databases } = useReadReplicasQuery({ projectRef })
const defaultHelper = getDefaultHelper(EXPLORER_DATEPICKER_HELPERS)
const [params, setParams] = useState<LogsEndpointParams>({
sql: initialParams?.sql || '',
@@ -1,15 +1,11 @@
import Reports from 'components/interfaces/Reports/Reports'
import { ReportsLayout } from 'components/layouts'
import EditReportModal from 'components/to-be-cleaned/Reports/EditReportModal'
import type { NextPageWithLayout } from 'types'
const PageLayout: NextPageWithLayout = () => (
<>
<div className="mx-auto flex flex-col gap-4 p-3">
<Reports />
</div>
<EditReportModal />
</>
<div className="mx-auto flex flex-col gap-4 p-3">
<Reports />
</div>
)
PageLayout.getLayout = (page) => <ReportsLayout>{page}</ReportsLayout>
@@ -25,6 +25,8 @@ import { ReportsLayout } from 'components/layouts'
import ShimmerLine from 'components/ui/ShimmerLine'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useSelectedOrganization } from 'hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
export const ApiReport: NextPageWithLayout = () => {
const report = useApiReport()
@@ -108,7 +110,12 @@ export const ApiReport: NextPageWithLayout = () => {
// hook to fetch data
const useApiReport = () => {
const { project } = useProjectContext()
const { ref: projectRef } = useParams()
const state = useDatabaseSelectorStateSnapshot()
// [Joshen] TODO: Once API support is out
const showReadReplicasUI = false // project?.is_read_replicas_enabled
const queryHooks = queriesFactory<keyof typeof PRESET_CONFIG.api.queries>(
PRESET_CONFIG.api.queries,
@@ -1,14 +1,9 @@
import { useParams } from 'common/hooks'
import dayjs from 'dayjs'
import { ArrowRight } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import {
AlertDescription_Shadcn_,
Alert_Shadcn_,
Button,
IconArrowRight,
IconExternalLink,
} from 'ui'
import { AlertDescription_Shadcn_, Alert_Shadcn_, Button, IconExternalLink } from 'ui'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants'
@@ -18,6 +13,7 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
import ChartHandler from 'components/to-be-cleaned/Charts/ChartHandler'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import Table from 'components/to-be-cleaned/Table'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import Panel from 'components/ui/Panel'
import { useDatabaseSizeQuery } from 'data/database/database-size-query'
import type { DbQueryHook } from 'hooks/analytics/useDbQuery'
@@ -45,19 +41,27 @@ const DatabaseUsage = () => {
const { project } = useProjectContext()
const [dateRange, setDateRange] = useState<any>(undefined)
const showReadReplicasUI = project?.is_read_replicas_enabled
const report = useDatabaseReport()
const { data } = useDatabaseSizeQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const databaseSizeBytes = data?.result[0].db_size ?? 0
const report = useDatabaseReport()
return (
<>
<div>
<section>
<Panel title={<h2>Database health</h2>}>
<Panel
title={
<div className="w-full flex items-center justify-between">
<h2>Database health</h2>
{showReadReplicasUI && <DatabaseSelector />}
</div>
}
>
<Panel.Content>
<div className="mb-4 flex items-center space-x-3">
<DateRangePicker
@@ -68,12 +72,12 @@ const DatabaseUsage = () => {
onChange={setDateRange}
/>
{dateRange && (
<div className="flex items-center space-x-2">
<div className="flex items-center gap-x-2">
<p className="text-foreground-light">
{dayjs(dateRange.period_start.date).format('MMMM D, hh:mma')}
</p>
<p className="text-foreground-light">
<IconArrowRight size={12} />
<ArrowRight size={12} />
</p>
<p className="text-foreground-light">
{dayjs(dateRange.period_end.date).format('MMMM D, hh:mma')}
@@ -1,50 +1,41 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { useParams } from 'common/hooks'
import { useParams } from 'common'
import { CreateReportModal } from 'components/interfaces/Reports/Reports.CreateReportModal'
import { ReportsLayout } from 'components/layouts'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import { Loading } from 'components/ui/Loading'
import { useContentQuery } from 'data/content/content-query'
import { useCheckPermissions } from 'hooks'
import { useProfile } from 'lib/profile'
import { useProjectContentStore } from 'stores/projectContentStore'
import type { NextPageWithLayout } from 'types'
export const UserReportPage: NextPageWithLayout = () => {
const [loading, setLoading] = useState(true)
const router = useRouter()
const { ref } = useParams()
const { profile } = useProfile()
const [showCreateReportModal, setShowCreateReportModal] = useState(false)
const contentStore = useProjectContentStore(ref)
const { isLoading } = useContentQuery(ref, {
onSuccess: (data) => {
const reports = data.content
.filter((x) => x.type === 'report')
.sort((a, b) => a.name.localeCompare(b.name))
if (reports.length > 1) router.push(`/project/${ref}/reports/${reports[0].id}`)
},
})
const canCreateReport = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
resource: { type: 'report', owner_id: profile?.id },
subject: { id: profile?.id },
})
async function loadReports() {
await contentStore.load()
const reports = contentStore.reports()
if (reports.length >= 1) {
router.push(`/project/${ref}/reports/${reports[0].id}`)
} else {
setLoading(false)
}
}
useEffect(() => {
loadReports()
}, [ref])
return (
<div className="mx-auto my-32 w-full max-w-7xl flex-grow space-y-16">
{loading ? (
{isLoading ? (
<Loading />
) : (
<>
@@ -67,12 +58,8 @@ export const UserReportPage: NextPageWithLayout = () => {
</ProductEmptyState>
<CreateReportModal
visible={showCreateReportModal}
onCancel={() => {
setShowCreateReportModal(false)
}}
afterSubmit={() => {
setShowCreateReportModal(false)
}}
onCancel={() => setShowCreateReportModal(false)}
afterSubmit={() => setShowCreateReportModal(false)}
/>
</>
)}
-199
View File
@@ -1,199 +0,0 @@
import { makeObservable, observable, computed } from 'mobx'
import { get, patch, post, delete_ } from 'lib/common/fetch'
import { keyBy } from 'lodash'
import type { UserContent } from 'types'
import { API_URL } from 'lib/constants'
/**
* Singleton
* Usage:
* import { useProjectContentStore } from 'stores/userContent/projectContentStore'
* const contentStore = useProjectContentStore(projectRef)
* contentStore.load() // inside usEffect()
* @deprecated use content store on RootStore instead.
*/
let store: any = null
let projectRef: any = null
export function useProjectContentStore(ref: any) {
if (store === null || ref !== projectRef) {
// set the current ref
projectRef = ref
const baseUrl = `${API_URL}/projects/${ref}/content`
store = new ProjectContent(baseUrl)
}
return store
}
/**
* Local type declarations
*/
interface UserContentMap {
[key: string]: UserContent
}
/**
* MobX Store
* @deprecated
*/
export default class ProjectContent {
STATES = {
INITIAL: 'initial',
LOADING: 'loading',
ERROR: 'error',
LOADED: 'loaded',
}
rootStore = null
url = ''
state = this.STATES.INITIAL
data: UserContentMap = {}
error = null
/**
* @param connectionString And encrypted database connection string to pass to the API
*/
constructor(dataUrl: any) {
this.url = dataUrl
makeObservable(this, {
state: observable,
data: observable,
list: observable,
hasError: computed,
isLoading: computed,
})
}
async fetchData() {
const headers = {
'Content-Type': 'application/json',
}
const res = await get(this.url, { headers })
if (res.error) {
throw res.error
}
this.data = res.data.length > 0 ? keyBy(res.data, 'id') : res.data
return res
}
async load() {
let { LOADING, ERROR, LOADED } = this.STATES
try {
this.error = null
this.state = LOADING
await this.fetchData()
this.state = LOADED
return this.data
} catch (e: any) {
console.error('e.message', e.message)
this.error = e
this.state = ERROR
}
}
get hasError() {
return this.state === this.STATES.ERROR
}
get isLoading() {
return this.state === this.STATES.INITIAL || this.state === this.STATES.LOADING
}
list(filter: any) {
let arr = Object.values(this.data)
if (!!filter) {
return arr.filter(filter).sort((a, b) => a.name.localeCompare(b.name))
} else {
return arr.sort((a, b) => a.name.localeCompare(b.name))
}
}
reports(filter: any) {
let arr = Object.values(this.data)
if (!!filter) {
return arr
.filter((x) => x.type == 'report')
.filter(filter)
.sort((a, b) => a.name.localeCompare(b.name))
} else {
return arr.filter((x) => x.type == 'report').sort((a, b) => a.name.localeCompare(b.name))
}
}
sqlSnippets(filter: any) {
let arr = Object.values(this.data)
if (!!filter) {
const arr_filtered = arr
.filter((x) => x.type == 'sql')
.filter(filter)
.sort((a, b) => a.name.localeCompare(b.name))
return arr_filtered
} else {
const arr_filtered = arr
.filter((x) => x.type == 'sql')
.sort((a, b) => a.name.localeCompare(b.name))
return arr_filtered
}
}
byId(id: any) {
return this.data[id]
}
async create(payload: any) {
try {
const headers = {
'Content-Type': 'application/json',
}
const created = await post(this.url, payload, { headers })
if (created.error) throw created.error
this.data[created['id']] = created
return { data: created, error: null }
} catch (error) {
return { data: null, error }
}
}
async update(id: any, updates: any, type: any) {
try {
const headers = {
'Content-Type': 'application/json',
}
let payload = {
...updates,
type,
id,
}
const url = `${this.url}?id=${id}`
const updated = await patch(url, payload, { headers })
if (updated.error) throw updated.error
this.data[id] = updated
return { data: updated, error: null }
} catch (error) {
return { data: null, error }
}
}
async updateSql(id: any, updates: any) {
return this.update(id, updates, 'sql')
}
async updateReport(id: any, updates: any) {
return this.update(id, updates, 'report')
}
async del(id: any) {
try {
const headers = {
'Content-Type': 'application/json',
}
const url = `${this.url}?id=${id}`
const deleted = await delete_(url, {}, { headers })
if (deleted.error) throw deleted.error
delete this.data[id]
return { data: true, error: null }
} catch (error) {
return { data: false, error }
}
}
}
+332 -38
View File
@@ -472,17 +472,41 @@ export interface paths {
get: operations['ProjectsController_getProjectByFlyExtensionId']
}
'/platform/projects/{ref}/content': {
/** Gets project's content */
/**
* Gets project's content
* @deprecated
*/
get: operations['ContentController_getContent']
/** Updates project's content */
put: operations['ContentController_updateWholeContent']
put: operations['ContentController_updateWholeContentV2']
/** Creates project's content */
post: operations['ContentController_createContent']
post: operations['ContentController_createContentV2']
/** Deletes project's contents */
delete: operations['ContentController_deleteContents']
/** Updates project's content */
/**
* Updates project's content
* @deprecated
*/
patch: operations['ContentController_updateContent']
}
'/platform/projects/{ref}/content/item/{id}': {
/** Gets project's content by the given id */
get: operations['ContentController_getContentById']
}
'/platform/projects/{ref}/content/folders': {
/** Gets project's content root folder */
get: operations['ContentFoldersController_getRootFolder']
/** Creates project's content folder */
post: operations['ContentFoldersController_createFolder']
/** Deletes project's content folders */
delete: operations['ContentFoldersController_DeleteFolder']
}
'/platform/projects/{ref}/content/folders/{id}': {
/** Gets project's content folder */
get: operations['ContentFoldersController_getFolder']
/** Updates project's content folder */
patch: operations['ContentFoldersController_updateFolder']
}
'/platform/projects/{ref}/daily-stats': {
/** Gets daily project stats */
get: operations['DailyStatsController_getDailyStats']
@@ -796,11 +820,11 @@ export interface paths {
post: operations['TelemetryPageController_sendServerPage']
}
'/platform/telemetry/activity': {
/** Sends mixpanel server activity */
/** Sends server activity */
post: operations['TelemetryActivityController_sendServerActivity']
}
'/platform/telemetry/pageview': {
/** Send mixpanel page event */
/** Send pageview event */
post: operations['TelemetryPageviewController_sendServerPageViewed']
}
'/platform/vercel/token': {
@@ -923,6 +947,11 @@ export interface paths {
post: operations['GithubSecretAlertController_resetJwt']
}
'/system/projects/{ref}/functions/{function_slug}': {
/**
* Delete a function
* @description Deletes a function with the specified slug from the specified project.
*/
delete: operations['SystemFunctionSlugController_deleteFunction']
/**
* Update a function
* @description Updates a function with the specified slug and project.
@@ -1328,17 +1357,27 @@ export interface paths {
get: operations['V0ProjectsMetricsController_getProjectsMetrics']
}
'/v0/projects/{ref}/content': {
/** Gets project's content */
/**
* Gets project's content
* @deprecated
*/
get: operations['ContentController_getContent']
/** Updates project's content */
put: operations['ContentController_updateWholeContent']
put: operations['ContentController_updateWholeContentV2']
/** Creates project's content */
post: operations['ContentController_createContent']
post: operations['ContentController_createContentV2']
/** Deletes project's contents */
delete: operations['ContentController_deleteContents']
/** Updates project's content */
/**
* Updates project's content
* @deprecated
*/
patch: operations['ContentController_updateContent']
}
'/v0/projects/{ref}/content/item/{id}': {
/** Gets project's content by the given id */
get: operations['ContentController_getContentById']
}
'/v0/projects/{ref}/databases': {
/** Gets non-removed databases of a specified project */
get: operations['DatabasesController_getDatabases']
@@ -3709,15 +3748,28 @@ export interface components {
description?: string
project_id: number
owner_id: number
last_updated_by: number
last_updated_by?: number
}
GetUserContentResponse: {
data: components['schemas']['GetUserContentObject'][]
}
CreateContentParams: {
GetUserContentByIdResponse: {
folder_id?: string
id: string
inserted_at: string
updated_at: string
type: Record<string, never>
visibility: Record<string, never>
name: string
description: string
description?: string
project_id: number
owner_id: number
last_updated_by?: number
}
CreateContentBody: {
id?: string
name: string
description?: string
/** @enum {string} */
type: 'sql' | 'report' | 'log_sql'
/** @enum {string} */
@@ -3735,21 +3787,34 @@ export interface components {
description?: string
project_id: number
owner_id: number
last_updated_by: number
last_updated_by?: number
}
UpsertContentParams: {
id: string
CreateContentBodyV2: {
id?: string
name: string
description: string
description?: string
/** @enum {string} */
type: 'sql' | 'report' | 'log_sql'
/** @enum {string} */
visibility: 'user' | 'project' | 'org' | 'public'
content?: Record<string, never>
owner_id?: number
project_id: number
folder_id?: string
}
UpdateContentParams: {
UserContentObjectV2: {
id: string
inserted_at: string
updated_at: string
type: Record<string, never>
visibility: Record<string, never>
name: string
description?: string
project_id: number
owner_id: number
last_updated_by?: number
folder_id?: string
}
UpdateContentBody: {
id?: string
name?: string
description?: string
@@ -3760,9 +3825,74 @@ export interface components {
content?: Record<string, never>
owner_id?: number
}
UpsertContentBody: {
id?: string
name: string
description?: string
/** @enum {string} */
type: 'sql' | 'report' | 'log_sql'
/** @enum {string} */
visibility: 'user' | 'project' | 'org' | 'public'
content?: Record<string, never>
owner_id?: number
project_id: number
}
UpsertContentBodyV2: {
id?: string
name: string
description?: string
/** @enum {string} */
type: 'sql' | 'report' | 'log_sql'
/** @enum {string} */
visibility: 'user' | 'project' | 'org' | 'public'
content?: Record<string, never>
owner_id?: number
project_id: number
folder_id?: string
}
BulkDeleteUserContentResponse: {
id: string
}
UserContentFolder: {
id: string
name: string
project_id: number
owner_id: number
parent_id?: string | null
}
UserContentObjectMeta: {
id: string
inserted_at: string
updated_at: string
type: Record<string, never>
visibility: Record<string, never>
name: string
description?: string
project_id: number
owner_id: number
last_updated_by?: number
folder_id?: string
}
GetUserContentFolderResponse: {
data: {
folders?: components['schemas']['UserContentFolder'][]
contents?: components['schemas']['UserContentObjectMeta'][]
}
}
CreateContentFolderBody: {
name: string
parent_id?: string
}
CreateUserContentFolderResponse: {
id: string
name: string
project_id: number
owner_id: number
parent_id?: string | null
}
UpdateContentFolderBody: {
name: string
}
DatabaseDetailResponse: {
/** @enum {string} */
status:
@@ -4557,7 +4687,7 @@ export interface components {
env_sync_error?: components['schemas']['SyncVercelEnvError']
}
UpdateVercelConnectionsBody: {
metadata: Record<string, never>
env_sync_targets?: ('production' | 'preview' | 'development')[]
}
DeleteVercelConnectionResponse: {
id: string
@@ -5663,7 +5793,7 @@ export interface components {
/** @enum {string} */
visibility: 'user' | 'project' | 'org' | 'public'
name: string
description: string | null
description?: string
project: components['schemas']['SnippetProject']
owner: components['schemas']['SnippetUser']
updated_by: components['schemas']['SnippetUser']
@@ -5685,7 +5815,7 @@ export interface components {
/** @enum {string} */
visibility: 'user' | 'project' | 'org' | 'public'
name: string
description: string | null
description?: string
project: components['schemas']['SnippetProject']
owner: components['schemas']['SnippetUser']
updated_by: components['schemas']['SnippetUser']
@@ -9350,7 +9480,10 @@ export interface operations {
}
}
}
/** Gets project's content */
/**
* Gets project's content
* @deprecated
*/
ContentController_getContent: {
parameters: {
path: {
@@ -9371,17 +9504,15 @@ export interface operations {
}
}
/** Updates project's content */
ContentController_updateWholeContent: {
ContentController_updateWholeContentV2: {
requestBody: {
content: {
'application/json': components['schemas']['UpsertContentParams']
'application/json': components['schemas']['UpsertContentBodyV2']
}
}
responses: {
200: {
content: {
'application/json': components['schemas']['UserContentObject']
}
content: never
}
/** @description Failed to update project's content */
500: {
@@ -9390,7 +9521,7 @@ export interface operations {
}
}
/** Creates project's content */
ContentController_createContent: {
ContentController_createContentV2: {
parameters: {
path: {
/** @description Project ref */
@@ -9399,13 +9530,13 @@ export interface operations {
}
requestBody: {
content: {
'application/json': components['schemas']['CreateContentParams']
'application/json': components['schemas']['CreateContentBodyV2']
}
}
responses: {
201: {
content: {
'application/json': components['schemas']['UserContentObject'][]
'application/json': components['schemas']['UserContentObjectV2']
}
}
/** @description Failed to create project's content */
@@ -9437,7 +9568,10 @@ export interface operations {
}
}
}
/** Updates project's content */
/**
* Updates project's content
* @deprecated
*/
ContentController_updateContent: {
parameters: {
query: {
@@ -9446,7 +9580,7 @@ export interface operations {
}
requestBody: {
content: {
'application/json': components['schemas']['UpdateContentParams']
'application/json': components['schemas']['UpdateContentBody']
}
}
responses: {
@@ -9461,6 +9595,140 @@ export interface operations {
}
}
}
/** Gets project's content by the given id */
ContentController_getContentById: {
parameters: {
path: {
/** @description Project ref */
ref: string
/** @description Content id */
id: string
}
}
responses: {
200: {
content: {
'application/json': components['schemas']['GetUserContentByIdResponse']
}
}
/** @description Failed to retrieve project's content by the given id */
500: {
content: never
}
}
}
/** Gets project's content root folder */
ContentFoldersController_getRootFolder: {
parameters: {
query?: {
type?: 'sql' | 'report' | 'log_sql'
}
path: {
/** @description Project ref */
ref: string
}
}
responses: {
200: {
content: {
'application/json': components['schemas']['GetUserContentFolderResponse']
}
}
/** @description Failed to retrieve project's content root folder */
500: {
content: never
}
}
}
/** Creates project's content folder */
ContentFoldersController_createFolder: {
parameters: {
path: {
/** @description Project ref */
ref: string
}
}
requestBody: {
content: {
'application/json': components['schemas']['CreateContentFolderBody']
}
}
responses: {
201: {
content: {
'application/json': components['schemas']['CreateUserContentFolderResponse']
}
}
/** @description Failed to create project's content folder */
500: {
content: never
}
}
}
/** Deletes project's content folders */
ContentFoldersController_DeleteFolder: {
parameters: {
query: {
ids: string[]
}
}
responses: {
200: {
content: never
}
/** @description Failed to delete project's content folders */
500: {
content: never
}
}
}
/** Gets project's content folder */
ContentFoldersController_getFolder: {
parameters: {
path: {
/** @description Project ref */
ref: string
/** @description Content folder id */
id: string
}
}
responses: {
200: {
content: {
'application/json': components['schemas']['GetUserContentFolderResponse']
}
}
/** @description Failed to retrieve project's content folder */
500: {
content: never
}
}
}
/** Updates project's content folder */
ContentFoldersController_updateFolder: {
parameters: {
path: {
/** @description Project ref */
ref: string
/** @description Content folder id */
id: string
}
}
requestBody: {
content: {
'application/json': components['schemas']['UpdateContentFolderBody']
}
}
responses: {
200: {
content: never
}
/** @description Failed to update project's content folder */
500: {
content: never
}
}
}
/** Gets daily project stats */
DailyStatsController_getDailyStats: {
parameters: {
@@ -11404,7 +11672,7 @@ export interface operations {
}
}
}
/** Sends mixpanel server activity */
/** Sends server activity */
TelemetryActivityController_sendServerActivity: {
requestBody: {
content: {
@@ -11415,13 +11683,13 @@ export interface operations {
201: {
content: never
}
/** @description Failed to send mixpanel server activity */
/** @description Failed to send server activity */
500: {
content: never
}
}
}
/** Send mixpanel page event */
/** Send pageview event */
TelemetryPageviewController_sendServerPageViewed: {
requestBody: {
content: {
@@ -11432,7 +11700,7 @@ export interface operations {
201: {
content: never
}
/** @description Failed to send mixpanel page event */
/** @description Failed to send pageview event */
500: {
content: never
}
@@ -11743,7 +12011,7 @@ export interface operations {
}
}
responses: {
200: {
204: {
content: never
}
/** @description Failed to update Vercel connection */
@@ -12120,6 +12388,32 @@ export interface operations {
}
}
}
/**
* Delete a function
* @description Deletes a function with the specified slug from the specified project.
*/
SystemFunctionSlugController_deleteFunction: {
parameters: {
path: {
/** @description Project ref */
ref: string
/** @description Function slug */
function_slug: string
}
}
responses: {
200: {
content: never
}
403: {
content: never
}
/** @description Failed to delete function with given slug */
500: {
content: never
}
}
}
/**
* Update a function
* @description Updates a function with the specified slug and project.
@@ -116,7 +116,7 @@ export const MultiSelectV2 = ({
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No options found</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
<ScrollArea className={cn(formattedOptions.length > 5 ? 'h-[225px]' : '')}>
<ScrollArea className={cn(formattedOptions.length > 7 ? 'h-[210px]' : '')}>
{formattedOptions?.map((option) => {
const active =
selectedOptions &&
+34
View File
@@ -0,0 +1,34 @@
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
*.tfvars
*.tfvars.json
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
+41
View File
@@ -0,0 +1,41 @@
variable "git_branch" {
type = string
default = null
}
# Fetch all branches of a linked project
data "supabase_branch" "all" {
parent_project_ref = var.linked_project
}
# Import an existing branch database
import {
for_each = {
for b in data.supabase_branch.all.branches :
b.git_branch => b
if b.git_branch == var.git_branch
}
to = supabase_branch.imported[0]
id = each.value.id
}
resource "supabase_branch" "imported" {
count = length(var.git_branch[*])
parent_project_ref = var.linked_project
git_branch = var.git_branch
}
# Override auth settings for the current branch
resource "supabase_settings" "preview" {
count = length(var.git_branch[*])
project_ref = supabase_branch.imported[0].database.id
auth = jsonencode({
site_url = "http://localhost:3001"
})
}
output "branch_database" {
value = one(supabase_branch.imported[*].database)
sensitive = true
}
+17
View File
@@ -0,0 +1,17 @@
# Define a linked project variable as user input
variable "linked_project" {
type = string
}
# Configure api settings for the linked project
resource "supabase_settings" "production" {
project_ref = var.linked_project
database = jsonencode({})
api = jsonencode({
db_schema = "public, storage, graphql_public"
db_extra_search_path = "public, extensions"
max_rows = 1000
})
}
+10
View File
@@ -0,0 +1,10 @@
terraform {
required_providers {
supabase = {
source = "supabase/supabase"
version = "~> 1.0"
}
}
}
provider "supabase" {}