Showing {offset + 1} to {offset + invoices.length} out of {count} invoices
diff --git a/studio/components/interfaces/Billing/Invoices.types.ts b/studio/components/interfaces/Billing/Invoices.types.ts
new file mode 100644
index 00000000000..97b8916c3e3
--- /dev/null
+++ b/studio/components/interfaces/Billing/Invoices.types.ts
@@ -0,0 +1,15 @@
+export enum InvoiceStatus {
+ DRAFT = 'draft',
+ PAID = 'paid',
+ VOID = 'void',
+ UNCOLLECTIBLE = 'uncollectible',
+ OPEN = 'open',
+}
+
+export type Invoice = {
+ id: string
+ number: string
+ period_end: number
+ subtotal: number
+ status: InvoiceStatus
+}
diff --git a/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx b/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx
index a7b6931b604..200f1fdd719 100644
--- a/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx
+++ b/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx
@@ -1,12 +1,15 @@
import { useState, useEffect } from 'react'
import { Button, Loading, IconFileText, IconDownload, IconChevronLeft, IconChevronRight } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
+import Link from 'next/link'
import { checkPermissions, useStore } from 'hooks'
import { API_URL } from 'lib/constants'
import { get, head } from 'lib/common/fetch'
import Table from 'components/to-be-cleaned/Table'
import NoPermission from 'components/ui/NoPermission'
+import InvoiceStatusBadge from 'components/interfaces/Billing/InvoiceStatusBadge'
+import { Invoice, InvoiceStatus } from 'components/interfaces/Billing/Invoices.types'
const PAGE_LIMIT = 10
@@ -18,7 +21,7 @@ const InvoicesSettings = () => {
const [page, setPage] = useState(1)
const [count, setCount] = useState(0)
- const [invoices, setInvoices] = useState([])
+ const [invoices, setInvoices] = useState([])
const { stripe_customer_id } = ui.selectedOrganization ?? {}
const offset = (page - 1) * PAGE_LIMIT
@@ -96,6 +99,9 @@ const InvoicesSettings = () => {
Date ,
Amount due ,
Invoice number ,
+
+ Status
+ ,
,
]}
body={
@@ -109,7 +115,7 @@ const InvoicesSettings = () => {
) : (
<>
- {invoices.map((x: any) => {
+ {invoices.map((x) => {
return (
@@ -124,11 +130,22 @@ const InvoicesSettings = () => {
{x.number}
+
+
+
+ {[InvoiceStatus.UNCOLLECTIBLE, InvoiceStatus.OPEN].includes(x.status) && (
+
+
+ Pay Now
+
+
+ )}
+
}
+ icon={
}
onClick={() => fetchInvoice(x.id)}
/>
@@ -137,7 +154,7 @@ const InvoicesSettings = () => {
)
})}
-
+
Showing {offset + 1} to {offset + invoices.length} out of {count} invoices
diff --git a/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx b/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx
index 3f73035e896..8dd437ebe3d 100644
--- a/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx
+++ b/studio/components/interfaces/Organization/TeamSettings/InviteMemberButton.tsx
@@ -1,24 +1,27 @@
import { isNil } from 'lodash'
-import { FC, useEffect, useState } from 'react'
+import { useEffect, useState } from 'react'
import { object, string } from 'yup'
import * as Tooltip from '@radix-ui/react-tooltip'
import { Button, Form, IconMail, Input, Modal, Select } from 'ui'
-import { Member, User, Role } from 'types'
+import { Member, Role } from 'types'
import { checkPermissions, useParams, useStore } from 'hooks'
-import { post } from 'lib/common/fetch'
-import { API_URL } from 'lib/constants'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useOrganizationMemberInviteCreateMutation } from 'data/organizations/organization-member-invite-create-mutation'
-interface Props {
- user: User
+export interface InviteMemberButtonProps {
+ userId: number
members: Member[]
roles: Role[]
rolesAddable: Number[]
}
-const InviteMemberButton: FC = ({ user, members = [], roles = [], rolesAddable = [] }) => {
+const InviteMemberButton = ({
+ userId,
+ members = [],
+ roles = [],
+ rolesAddable = [],
+}: InviteMemberButtonProps) => {
const { ui } = useStore()
const { slug } = useParams()
@@ -67,7 +70,7 @@ const InviteMemberButton: FC = ({ user, members = [], roles = [], rolesAd
const response = await mutateAsync({
slug,
invitedEmail: values.email.toLowerCase(),
- ownerId: user.id,
+ ownerId: userId,
roleId,
})
diff --git a/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx b/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx
index 925ddf81cc7..f9e5e0b40ed 100644
--- a/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx
+++ b/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx
@@ -8,28 +8,29 @@ import { Member, Role } from 'types'
import { useStore, useParams } from 'hooks'
import { isInviteExpired, getUserDisplayName } from '../Organization.utils'
+import { useProfileQuery } from 'data/profile/profile-query'
+import { useOrganizationMemberUpdateMutation } from 'data/organizations/organization-member-update-mutation'
import Table from 'components/to-be-cleaned/Table'
import MemberActions from './MemberActions'
import RolesHelperModal from './RolesHelperModal/RolesHelperModal'
import { getRolesManagementPermissions } from './TeamSettings.utils'
-import { useOrganizationMemberUpdateMutation } from 'data/organizations/organization-member-update-mutation'
interface SelectedMember extends Member {
oldRoleId: number
newRoleId: number
}
-interface Props {
+export interface MembersViewProps {
roles: Role[]
members: Member[]
searchString: string
}
-const MembersView: FC = ({ searchString, roles, members }) => {
+const MembersView = ({ searchString, roles, members }: MembersViewProps) => {
const { ui } = useStore()
const { slug } = useParams()
- const user = ui.profile
+ const { data: profile } = useProfileQuery()
const { rolesAddable, rolesRemovable } = getRolesManagementPermissions(roles)
@@ -108,7 +109,7 @@ const MembersView: FC = ({ searchString, roles, members }) => {
...filteredMembers.map((x: Member, i: number) => {
const [memberRoleId] = x.role_ids ?? []
const role = (roles || []).find((role) => role.id === memberRoleId)
- const memberIsUser = x.primary_email == user?.primary_email
+ const memberIsUser = x.primary_email == profile?.primary_email
const memberIsPendingInvite = !!x.invited_id
const canRemoveRole = rolesRemovable.includes(memberRoleId)
const disableRoleEdit = !canRemoveRole || memberIsUser || memberIsPendingInvite
diff --git a/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx b/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx
index 98212c2153a..9289a210943 100644
--- a/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx
+++ b/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx
@@ -6,6 +6,7 @@ import * as Tooltip from '@radix-ui/react-tooltip'
import { useStore, useParams } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
+import { useProfileQuery } from 'data/profile/profile-query'
import InviteMemberButton from './InviteMemberButton'
import MembersView from './MembersView'
import { getRolesManagementPermissions, hasMultipleOwners } from './TeamSettings.utils'
@@ -17,7 +18,7 @@ const TeamSettings = () => {
const { ui } = useStore()
const { slug } = useParams()
- const user = ui.profile
+ const { data: profile } = useProfileQuery()
const isOwner = ui.selectedOrganization?.is_owner
const { data: detailData } = useOrganizationDetailQuery({ slug })
@@ -73,10 +74,10 @@ const TeamSettings = () => {
placeholder="Filter members"
/>
- {canAddMembers && user !== undefined && (
+ {canAddMembers && profile !== undefined && (
{
- const { ui } = useStore()
-
- const router = useRouter()
- const { id, ref } = router.query
+ const { id, ref } = useParams()
+ const { data: profile } = useProfileQuery()
const [report, setReport] = useState()
@@ -53,7 +51,7 @@ const Reports = () => {
visibility: report?.visibility,
owner_id: report?.owner_id,
},
- subject: { id: ui.profile?.id },
+ subject: { id: profile?.id },
})
const canUpdateReport = checkPermissions(PermissionAction.UPDATE, 'user_content', {
resource: {
@@ -61,7 +59,7 @@ const Reports = () => {
visibility: report?.visibility,
owner_id: report?.owner_id,
},
- subject: { id: ui.profile?.id },
+ subject: { id: profile?.id },
})
/*
diff --git a/studio/components/interfaces/SQLEditor/TabSqlQuery/FavouriteButton.tsx b/studio/components/interfaces/SQLEditor/TabSqlQuery/FavouriteButton.tsx
index 19ab8769d49..59cd87f4141 100644
--- a/studio/components/interfaces/SQLEditor/TabSqlQuery/FavouriteButton.tsx
+++ b/studio/components/interfaces/SQLEditor/TabSqlQuery/FavouriteButton.tsx
@@ -5,11 +5,12 @@ import { Button, IconHeart } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useStore } from 'hooks'
import Telemetry from 'lib/telemetry'
+import { useProfileQuery } from 'data/profile/profile-query'
import { useSqlStore } from 'localStores/sqlEditor/SqlEditorStore'
const FavouriteButton = () => {
const { ui, content: contentStore } = useStore()
- const { profile: user } = ui
+ const { data: profile } = useProfileQuery()
const sqlEditorStore: any = useSqlStore()
@@ -52,7 +53,7 @@ const FavouriteButton = () => {
/*
* reload sql data in store and re-select tab
*/
- sqlEditorStore.loadTabs(sqlEditorStore.tabsFromContentStore(contentStore, user?.id), false)
+ sqlEditorStore.loadTabs(sqlEditorStore.tabsFromContentStore(contentStore, profile?.id), false)
sqlEditorStore.selectTab(id)
setLoading(false)
@@ -93,7 +94,7 @@ const FavouriteButton = () => {
/*
* reload sql data in store and re-select tab
*/
- sqlEditorStore.loadTabs(sqlEditorStore.tabsFromContentStore(contentStore, user?.id), false)
+ sqlEditorStore.loadTabs(sqlEditorStore.tabsFromContentStore(contentStore, profile?.id), false)
sqlEditorStore.selectTab(id)
setLoading(false)
} catch (error: any) {
diff --git a/studio/components/interfaces/SQLEditor/TabSqlQuery/UtilityActions.tsx b/studio/components/interfaces/SQLEditor/TabSqlQuery/UtilityActions.tsx
index 1dd3270dba5..fa06f309151 100644
--- a/studio/components/interfaces/SQLEditor/TabSqlQuery/UtilityActions.tsx
+++ b/studio/components/interfaces/SQLEditor/TabSqlQuery/UtilityActions.tsx
@@ -1,27 +1,26 @@
-import { FC } from 'react'
-import { observer } from 'mobx-react-lite'
import { Button, IconAlertCircle } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useKeyboardShortcuts, useStore, checkPermissions } from 'hooks'
+import { useKeyboardShortcuts, checkPermissions } from 'hooks'
import { IS_PLATFORM } from 'lib/constants'
+import { useProfileQuery } from 'data/profile/profile-query'
import { useSqlStore } from 'localStores/sqlEditor/SqlEditorStore'
import SavingIndicator from './SavingIndicator'
import FavouriteButton from './FavouriteButton'
import SizeToggleButton from './SizeToggleButton'
-interface Props {
+export interface UtilityActionsProps {
updateSqlSnippet: (value: any) => void
}
-const UtilityActions: FC = ({ updateSqlSnippet }) => {
- const { ui } = useStore()
+const UtilityActions = ({ updateSqlSnippet }: UtilityActionsProps) => {
+ const { data: profile } = useProfileQuery()
const sqlEditorStore: any = useSqlStore()
const canCreateSQLSnippet = checkPermissions(PermissionAction.CREATE, 'user_content', {
- resource: { type: 'sql', owner_id: ui.profile?.id },
- subject: { id: ui.profile?.id },
+ resource: { type: 'sql', owner_id: profile?.id },
+ subject: { id: profile?.id },
})
useKeyboardShortcuts(
@@ -80,4 +79,4 @@ const UtilityActions: FC = ({ updateSqlSnippet }) => {
)
}
-export default observer(UtilityActions)
+export default UtilityActions
diff --git a/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx b/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
index d29400e1a1b..3707cf06f8d 100644
--- a/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
+++ b/studio/components/interfaces/Settings/Logs/LogsQueryPanel.tsx
@@ -12,9 +12,10 @@ import {
import DatePickers from './Logs.DatePickers'
import Link from 'next/link'
import React from 'react'
-import { checkPermissions, useStore } from 'hooks'
+import { checkPermissions } from 'hooks'
+import { useProfileQuery } from 'data/profile/profile-query'
-interface Props {
+export interface LogsQueryPanelProps {
templates?: LogTemplate[]
onSelectTemplate: (template: LogTemplate) => void
onSelectSource: (source: LogsTableName) => void
@@ -29,7 +30,7 @@ interface Props {
warnings: LogsWarning[]
}
-const LogsQueryPanel: React.FC = ({
+const LogsQueryPanel = ({
templates = [],
onSelectTemplate,
hasEditorValue,
@@ -42,18 +43,15 @@ const LogsQueryPanel: React.FC = ({
defaultTo,
onDateChange,
warnings,
-}) => {
- const { ui } = useStore()
+}: LogsQueryPanelProps) => {
+ const { data: profile } = useProfileQuery()
const canCreateLogQuery = checkPermissions(PermissionAction.CREATE, 'user_content', {
- resource: { type: 'log_sql', owner_id: ui.profile?.id },
- subject: { id: ui.profile?.id },
+ resource: { type: 'log_sql', owner_id: profile?.id },
+ subject: { id: profile?.id },
})
return (
-
+
diff --git a/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx b/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx
index 57dac2a1607..bee4b202877 100644
--- a/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx
+++ b/studio/components/layouts/SQLEditorLayout/SQLEditorLayout.tsx
@@ -1,27 +1,28 @@
-import { FC, ReactNode, useEffect } from 'react'
+import { ReactNode, useEffect } from 'react'
import { observer } from 'mobx-react-lite'
import { useStore, withAuth } from 'hooks'
+import { useProfileQuery } from 'data/profile/profile-query'
import Error from 'components/ui/Error'
import { useSqlEditorStore, SqlEditorContext } from 'localStores/sqlEditor/SqlEditorStore'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import SQLEditorMenu from './SQLEditorMenu'
-interface Props {
+export interface SQLEditorLayoutProps {
title: string
children: ReactNode
}
-const SQLEditorLayout: FC
= ({ title, children }) => {
+const SQLEditorLayout = ({ title, children }: SQLEditorLayoutProps) => {
const { ui, content, meta } = useStore()
- const { profile: user } = ui
+ const { data: profile } = useProfileQuery()
const sqlEditorStore = useSqlEditorStore(ui.selectedProject?.ref, meta)
useEffect(() => {
if (sqlEditorStore) {
// this calls content.load() for us, as well as loading tabs
- sqlEditorStore.loadRemotePersistentData(content, user?.id)
+ sqlEditorStore.loadRemotePersistentData(content, profile?.id)
}
}, [sqlEditorStore])
diff --git a/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx b/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
index 262e106b899..699839ab3df 100644
--- a/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
+++ b/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
@@ -20,6 +20,7 @@ import { useOptimisticSqlSnippetCreate, useStore, checkPermissions } from 'hooks
import { IS_PLATFORM } from 'lib/constants'
import QueryTab from 'localStores/sqlEditor/QueryTab'
import { useSqlStore, TAB_TYPES } from 'localStores/sqlEditor/SqlEditorStore'
+import { useProfileQuery } from 'data/profile/profile-query'
import RenameQuery from 'components/to-be-cleaned/SqlEditor/RenameQuery'
import ConfirmationModal from 'components/ui/ConfirmationModal'
@@ -45,10 +46,8 @@ const OpenQueryItem = observer(
)
const DropdownMenu = observer(({ tabInfo }: { tabInfo: QueryTab }) => {
- const {
- ui: { profile: user },
- content: contentStore,
- } = useStore()
+ const { data: profile } = useProfileQuery()
+ const { content: contentStore } = useStore()
const sqlEditorStore: any = useSqlStore()
@@ -114,7 +113,7 @@ const DropdownMenu = observer(({ tabInfo }: { tabInfo: QueryTab }) => {
await contentStore.del(id)
sqlEditorStore.loadTabs(
- sqlEditorStore.tabsFromContentStore(contentStore, user?.id),
+ sqlEditorStore.tabsFromContentStore(contentStore, profile?.id),
false
)
}}
@@ -129,13 +128,13 @@ const DropdownMenu = observer(({ tabInfo }: { tabInfo: QueryTab }) => {
})
const SideBarContent = observer(() => {
- const { ui } = useStore()
+ const { data: profile } = useProfileQuery()
const sqlEditorStore: any = useSqlStore()
const [filterString, setFilterString] = useState('')
const canCreateSQLSnippet = checkPermissions(PermissionAction.CREATE, 'user_content', {
- resource: { type: 'sql', owner_id: ui.profile?.id },
- subject: { id: ui.profile?.id },
+ resource: { type: 'sql', owner_id: profile?.id },
+ subject: { id: profile?.id },
})
const handleNewQuery = useOptimisticSqlSnippetCreate(canCreateSQLSnippet)
diff --git a/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js b/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js
index f5cd6f030ea..4c16b6571b7 100644
--- a/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js
+++ b/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js
@@ -12,13 +12,15 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useKeyboardShortcuts, useStore, useWindowDimensions, checkPermissions } from 'hooks'
import Telemetry from 'lib/telemetry'
import { copyToClipboard, timeout } from 'lib/helpers'
+import { useProfileQuery } from 'data/profile/profile-query'
import { useSqlStore, UTILITY_TAB_TYPES } from 'localStores/sqlEditor/SqlEditorStore'
import { SQL_SNIPPET_SCHEMA_VERSION } from './SqlEditor.constants'
import UtilityActions from 'components/interfaces/SQLEditor/TabSqlQuery/UtilityActions'
const TabSqlQuery = observer(() => {
const sqlEditorStore = useSqlStore()
- const { ui, content: contentStore } = useStore()
+ const { data: profile } = useProfileQuery()
+ const { content: contentStore } = useStore()
const { height: screenHeight } = useWindowDimensions()
const snapOffset = 50
@@ -27,8 +29,8 @@ const TabSqlQuery = observer(() => {
const offset = 3
const canCreateSQLSnippet = checkPermissions(PermissionAction.CREATE, 'user_content', {
- resource: { type: 'sql', owner_id: ui.profile?.id },
- subject: { id: ui.profile?.id },
+ resource: { type: 'sql', owner_id: profile?.id },
+ subject: { id: profile?.id },
})
useEffect(() => {
diff --git a/studio/components/to-be-cleaned/SqlEditor/TabWelcome.js b/studio/components/to-be-cleaned/SqlEditor/TabWelcome.js
index 9faa88f801b..5ca96a1c348 100644
--- a/studio/components/to-be-cleaned/SqlEditor/TabWelcome.js
+++ b/studio/components/to-be-cleaned/SqlEditor/TabWelcome.js
@@ -5,15 +5,17 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import Telemetry from 'lib/telemetry'
import { useOptimisticSqlSnippetCreate, checkPermissions, useStore } from 'hooks'
+import { useProfileQuery } from 'data/profile/profile-query'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.constants'
import CardButton from 'components/ui/CardButton'
const TabWelcome = observer(() => {
const { ui } = useStore()
+ const { data: profile } = useProfileQuery()
const [sql, quickStart] = partition(SQL_TEMPLATES, { type: 'template' })
const canCreateSQLSnippet = checkPermissions(PermissionAction.CREATE, 'user_content', {
- resource: { type: 'sql', owner_id: ui.profile?.id },
- subject: { id: ui.profile?.id },
+ resource: { type: 'sql', owner_id: profile?.id },
+ subject: { id: profile?.id },
})
const handleNewQuery = useOptimisticSqlSnippetCreate(canCreateSQLSnippet)
diff --git a/studio/components/ui/Flag/FlagProvider.tsx b/studio/components/ui/Flag/FlagProvider.tsx
index 40d1ac7dae8..187f80e9bd9 100644
--- a/studio/components/ui/Flag/FlagProvider.tsx
+++ b/studio/components/ui/Flag/FlagProvider.tsx
@@ -1,17 +1,15 @@
-import { FC, useEffect, useState } from 'react'
-import { observer } from 'mobx-react-lite'
+import { User } from '@supabase/supabase-js'
+import { PropsWithChildren, useEffect, useState } from 'react'
import * as configcat from 'configcat-js'
-import { User } from 'types'
-import { useStore } from 'hooks'
import { IS_PLATFORM } from 'lib/constants'
import FlagContext from './FlagContext'
+import { useUser } from 'lib/auth'
let client: configcat.IConfigCatClient
-const FlagProvider: FC = ({ children }) => {
- const { ui } = useStore()
- const { profile } = ui
+const FlagProvider = ({ children }: PropsWithChildren<{}>) => {
+ const user = useUser()
const { Provider } = FlagContext
const [store, setStore] = useState({})
@@ -27,8 +25,8 @@ const FlagProvider: FC = ({ children }) => {
const flagStore: any = {}
const flagValues =
- user !== undefined
- ? await client.getAllValuesAsync(new configcat.User(user.primary_email))
+ user?.email !== undefined
+ ? await client.getAllValuesAsync(new configcat.User(user.email))
: await client.getAllValuesAsync()
flagValues.forEach((item: any) => {
flagStore[item.settingKey] = item.settingValue
@@ -41,10 +39,10 @@ const FlagProvider: FC = ({ children }) => {
// as per https://configcat.com/docs/sdk-reference/js/#polling-modes:
// The polling downloads the config.json at the set interval and are stored in the internal cache
// which subsequently all getValueAsync() calls are served from there
- if (IS_PLATFORM) getFlags(profile)
- }, [profile])
+ if (IS_PLATFORM) getFlags(user ?? undefined)
+ }, [user])
return {children}
}
-export default observer(FlagProvider)
+export default FlagProvider
diff --git a/studio/components/ui/SparkBar.tsx b/studio/components/ui/SparkBar.tsx
index a4f45c1ce30..4d89e5f80ad 100644
--- a/studio/components/ui/SparkBar.tsx
+++ b/studio/components/ui/SparkBar.tsx
@@ -1,3 +1,4 @@
+import clsx from 'clsx'
import { FC } from 'react'
interface Props {
@@ -30,10 +31,15 @@ const SparkBar: FC = ({
{hasLabels && (
-
+ 0 && 'max-w-[75%]'
+ )}
+ >
{labelBottom}
-
-
{labelTop}
+
+
{labelTop}
)}
>
+
+export const useEdgeFunctionUpdateMutation = ({
+ onSuccess,
+ ...options
+}: Omit<
+ UseMutationOptions
,
+ 'mutationFn'
+> = {}) => {
+ const queryClient = useQueryClient()
+
+ return useMutation(
+ (vars) => updateEdgeFunction(vars),
+ {
+ async onSuccess(data, variables, context) {
+ const { projectRef, slug } = variables
+ await queryClient.invalidateQueries(edgeFunctionsKeys.detail(projectRef, slug))
+ await onSuccess?.(data, variables, context)
+ },
+ ...options,
+ }
+ )
+}
diff --git a/studio/data/profile/profile-query.ts b/studio/data/profile/profile-query.ts
index d48256ef0d3..479cb7233aa 100644
--- a/studio/data/profile/profile-query.ts
+++ b/studio/data/profile/profile-query.ts
@@ -40,7 +40,10 @@ export const useProfileQuery = ({
useQuery(
profileKeys.profile(),
({ signal }) => getProfile(signal),
- options
+ {
+ staleTime: 1000 * 60 * 30, // default good for 30 mins
+ ...options,
+ }
)
export const useProfilePrefetch = () => {
diff --git a/studio/hooks/misc/useOptimisticSqlSnippetCreate.ts b/studio/hooks/misc/useOptimisticSqlSnippetCreate.ts
index aaf7c304491..8791a61ee86 100644
--- a/studio/hooks/misc/useOptimisticSqlSnippetCreate.ts
+++ b/studio/hooks/misc/useOptimisticSqlSnippetCreate.ts
@@ -1,11 +1,12 @@
import { createSqlSnippetSkeleton } from 'components/to-be-cleaned/SqlEditor/SqlEditor.utils'
+import { useProfileQuery } from 'data/profile/profile-query'
import { useSqlStore } from 'localStores/sqlEditor/SqlEditorStore'
import { useCallback } from 'react'
import { useStore } from './useStore'
export function useOptimisticSqlSnippetCreate(canCreateSQLSnippet: boolean) {
const { ui, content: contentStore } = useStore()
- const { profile: user } = ui
+ const { data: profile } = useProfileQuery()
const sqlEditorStore: any = useSqlStore()
@@ -14,13 +15,16 @@ export function useOptimisticSqlSnippetCreate(canCreateSQLSnippet: boolean) {
// get currently selected tab id in case we need to roll back to it
const previouslySelectedTabId = sqlEditorStore.selectedTabId
- const snippet = createSqlSnippetSkeleton({ owner_id: user?.id, ...args })
+ const snippet = createSqlSnippetSkeleton({
+ owner_id: profile?.id,
+ ...args,
+ })
// save the snippet in memory in the content store with a client-generated id
const { data } = contentStore.createOptimistically(snippet)
// update the tabs in the sql editor storage to match the content store
- const tabs = sqlEditorStore.tabsFromContentStore(contentStore, user?.id)
+ const tabs = sqlEditorStore.tabsFromContentStore(contentStore, profile?.id)
sqlEditorStore.loadTabs(tabs, false)
// select tab with new snippet
@@ -42,6 +46,6 @@ export function useOptimisticSqlSnippetCreate(canCreateSQLSnippet: boolean) {
})
}
},
- [user, sqlEditorStore, contentStore]
+ [profile, sqlEditorStore, contentStore]
)
}
diff --git a/studio/hooks/misc/withAuth.tsx b/studio/hooks/misc/withAuth.tsx
index d2cdc5d94fd..787b0ab730e 100644
--- a/studio/hooks/misc/withAuth.tsx
+++ b/studio/hooks/misc/withAuth.tsx
@@ -1,13 +1,13 @@
import Head from 'next/head'
-import { NextRouter, useRouter } from 'next/router'
+import { useRouter } from 'next/router'
import { ComponentType, useEffect } from 'react'
import { usePermissionsQuery } from 'data/permissions/permissions-query'
-import { useProfileQuery } from 'data/profile/profile-query'
-import { useStore } from 'hooks'
+import { useParams, useStore } from 'hooks'
+import { useAuth } from 'lib/auth'
import { IS_PLATFORM } from 'lib/constants'
import { getReturnToPath, STORAGE_KEY } from 'lib/gotrue'
-import { NextPageWithLayout } from 'types'
+import { isNextPageWithLayout, NextPageWithLayout } from 'types'
import Error500 from '../../pages/500'
const PLATFORM_ONLY_PAGES = [
@@ -22,32 +22,26 @@ export function withAuth(
WrappedComponent: ComponentType | NextPageWithLayout,
options?: {
redirectTo: string
+ /* run the redirect if the user is logged in */
redirectIfFound?: boolean
}
) {
const WithAuthHOC: ComponentType = (props: any) => {
const router = useRouter()
+ const { ref, slug } = useParams()
const rootStore = useStore()
+ const { isLoading, session } = useAuth()
- const { ref, slug } = router.query
const { app, ui } = rootStore
const page = router.pathname.split('/').slice(3).join('/')
const redirectTo = options?.redirectTo ?? defaultRedirectTo(ref)
const redirectIfFound = options?.redirectIfFound
- const {
- data: profile,
- isLoading,
- error,
- } = useProfileQuery({
- onSuccess(profile) {
- ui.setProfile(profile)
-
- if (!app.organizations.isInitialized) app.organizations.load()
- if (!app.projects.isInitialized) app.projects.load()
- },
- })
+ useEffect(() => {
+ if (!app.organizations.isInitialized) app.organizations.load()
+ if (!app.projects.isInitialized) app.projects.load()
+ }, [app.organizations.isInitialized, app.projects.isInitialized])
usePermissionsQuery({
enabled: IS_PLATFORM,
@@ -56,12 +50,14 @@ export function withAuth(
},
})
+ const isLoggedIn = Boolean(session)
+
const isAccessingBlockedPage =
!IS_PLATFORM &&
PLATFORM_ONLY_PAGES.some((platformOnlyPage) => page.startsWith(platformOnlyPage))
const isRedirecting =
isAccessingBlockedPage ||
- checkRedirectTo(isLoading, router, profile, error, redirectTo, redirectIfFound)
+ checkRedirectTo(isLoading, router.pathname, isLoggedIn, redirectTo, redirectIfFound)
useEffect(() => {
// This should run after setting store data
@@ -73,13 +69,13 @@ export function withAuth(
useEffect(() => {
if (router.isReady) {
if (ref) {
- rootStore.setProjectRef(Array.isArray(ref) ? ref[0] : ref)
+ rootStore.setProjectRef(ref)
}
- rootStore.setOrganizationSlug(slug ? String(slug) : undefined)
+ rootStore.setOrganizationSlug(slug)
}
}, [isLoading, router.isReady, ref, slug])
- if (!isLoading && !isRedirecting && !profile && error) {
+ if (!isLoading && !isRedirecting && !isLoggedIn) {
return
}
@@ -101,10 +97,10 @@ export function withAuth(
)
}
- WithAuthHOC.displayName = `WithAuth(${WrappedComponent.displayName})`
+ WithAuthHOC.displayName = `withAuth(${WrappedComponent.displayName})`
- if ('getLayout' in WrappedComponent) {
- ;(WithAuthHOC as any).getLayout = WrappedComponent.getLayout
+ if (isNextPageWithLayout(WrappedComponent)) {
+ ;(WithAuthHOC as NextPageWithLayout).getLayout = WrappedComponent.getLayout
}
return WithAuthHOC
@@ -116,20 +112,19 @@ function defaultRedirectTo(ref: string | string[] | undefined) {
function checkRedirectTo(
loading: boolean,
- router: NextRouter,
- profile: any,
- profileError: any,
+ pathname: string,
+ isLoggedIn: boolean,
redirectTo: string,
redirectIfFound?: boolean
) {
if (loading) return false
- if (router.pathname == redirectTo) return false
+ if (pathname === redirectTo) return false
// If redirectTo is set, redirect if the user is not logged in.
- if (redirectTo && !redirectIfFound && profileError?.code === 401) return true
+ if (redirectTo && !redirectIfFound && !isLoggedIn) return true
// If redirectIfFound is also set, redirect if the user was found
- if (redirectIfFound && profile) return true
+ if (redirectIfFound && isLoggedIn) return true
return false
}
diff --git a/studio/lib/auth.tsx b/studio/lib/auth.tsx
new file mode 100644
index 00000000000..a2066c37ff0
--- /dev/null
+++ b/studio/lib/auth.tsx
@@ -0,0 +1,144 @@
+import { Session } from '@supabase/supabase-js'
+import { useProfileQuery } from 'data/profile/profile-query'
+import { useStore } from 'hooks'
+import { auth } from 'lib/gotrue'
+import {
+ createContext,
+ PropsWithChildren,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react'
+import { GOTRUE_ERRORS } from './constants'
+
+/* Auth Context */
+
+export type AuthContext = { refreshSession: () => Promise } & (
+ | {
+ session: Session
+ isLoading: false
+ }
+ | {
+ session: null
+ isLoading: true
+ }
+ | {
+ session: null
+ isLoading: false
+ }
+)
+
+export const AuthContext = createContext({
+ session: null,
+ isLoading: true,
+ refreshSession: () => Promise.resolve(null),
+})
+
+export type AuthProviderProps = {}
+
+export const AuthProvider = ({ children }: PropsWithChildren) => {
+ const { ui } = useStore()
+ const [session, setSession] = useState(null)
+ const [isLoading, setIsLoading] = useState(true)
+
+ // Check for unverified GitHub users after a GitHub sign in
+ useEffect(() => {
+ async function handleEmailVerificationError() {
+ const { error } = await auth.initialize()
+
+ if (error?.message === GOTRUE_ERRORS.UNVERIFIED_GITHUB_USER) {
+ ui.setNotification({
+ category: 'error',
+ message:
+ 'Please verify your email on GitHub first, then reach out to us at support@supabase.io to log into the dashboard',
+ })
+ }
+ }
+
+ handleEmailVerificationError()
+ }, [])
+
+ // Setup a possible existing session
+ useEffect(() => {
+ let mounted = true
+
+ auth
+ .getSession()
+ .then(({ data: { session } }) => {
+ if (mounted) {
+ if (session) {
+ setSession(session)
+ }
+
+ setIsLoading(false)
+ }
+ })
+ .catch(() => {
+ if (mounted) {
+ setIsLoading(false)
+ }
+ })
+
+ return () => {
+ mounted = false
+ }
+ }, [])
+
+ // Keep the session in sync
+ useEffect(() => {
+ const {
+ data: { subscription },
+ } = auth.onAuthStateChange((_event, session) => {
+ setSession(session)
+ setIsLoading(false)
+ })
+
+ return subscription.unsubscribe
+ }, [])
+
+ // Track telemetry for the current user
+ useProfileQuery({
+ onSuccess(profile) {
+ ui.setProfile(profile)
+ },
+ // Never rerun the query
+ staleTime: Infinity,
+ cacheTime: Infinity,
+ })
+
+ // Helper method to refresh the session.
+ // For example after a user updates their profile
+ const refreshSession = useCallback(async () => {
+ const {
+ data: { session },
+ } = await auth.refreshSession()
+
+ return session
+ }, [])
+
+ const value = useMemo(() => {
+ if (session) {
+ return { session, isLoading: false, refreshSession } as const
+ }
+
+ return { session: null, isLoading: isLoading, refreshSession } as const
+ }, [session, isLoading, refreshSession])
+
+ return {children}
+}
+
+/* Auth Utils */
+
+export const useAuth = () => useContext(AuthContext)
+
+export const useSession = () => useAuth().session
+
+export const useUser = () => useSession()?.user ?? null
+
+export const useIsLoggedIn = () => {
+ const user = useUser()
+
+ return user !== null
+}
diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx
index 9f03a99259e..96d5f954a5d 100644
--- a/studio/pages/_app.tsx
+++ b/studio/pages/_app.tsx
@@ -32,8 +32,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { RootStore } from 'stores'
import HCaptchaLoadedStore from 'stores/hcaptcha-loaded-store'
import { StoreProvider } from 'hooks'
-import { GOTRUE_ERRORS } from 'lib/constants'
-import { auth } from 'lib/gotrue'
+import { AuthProvider } from 'lib/auth'
import { dart } from 'lib/constants/prism'
import { useRootQueryClient } from 'data/query-client'
@@ -49,26 +48,10 @@ dayjs.extend(relativeTime)
dart(Prism)
-function MyApp({ Component, pageProps }: AppPropsWithLayout) {
+function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
const queryClient = useRootQueryClient()
const [rootStore] = useState(() => new RootStore())
- useEffect(() => {
- async function handleEmailVerificationError() {
- const { error } = await auth.initialize()
-
- if (error?.message === GOTRUE_ERRORS.UNVERIFIED_GITHUB_USER) {
- rootStore.ui.setNotification({
- category: 'error',
- message:
- 'Please verify your email on GitHub first, then reach out to us at support@supabase.io to log into the dashboard',
- })
- }
- }
-
- handleEmailVerificationError()
- }, [])
-
const getSavingState = () => rootStore.content.savingState
// prompt the user if they try and leave with unsaved content store changes
@@ -108,39 +91,42 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
-
-
- Supabase
-
-
+
+
+
+ Supabase
+
+
-
-
+
-
-
- {getLayout( )}
-
-
+
+
+ {getLayout( )}
+
+
-
-
-
-
+
+
+
+
+
)
}
-export default MyApp
+
+export default CustomApp
diff --git a/studio/pages/account/me.tsx b/studio/pages/account/me.tsx
index e5cdeeeb8bd..6c00e76aa4e 100644
--- a/studio/pages/account/me.tsx
+++ b/studio/pages/account/me.tsx
@@ -1,14 +1,13 @@
import { observer } from 'mobx-react-lite'
-import { useEffect, useState } from 'react'
import { Button, IconMoon, IconSun, Input, Listbox } from 'ui'
-import { Session } from '@supabase/supabase-js'
import { AccountLayout } from 'components/layouts'
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
import Panel from 'components/ui/Panel'
+import { Profile as ProfileType, useProfileQuery } from 'data/profile/profile-query'
import { useProfileUpdateMutation } from 'data/profile/profile-update-mutation'
import { useStore } from 'hooks'
-import { auth } from 'lib/gotrue'
+import { useSession } from 'lib/auth'
import Link from 'next/link'
import { NextPageWithLayout } from 'types'
@@ -38,12 +37,14 @@ export default User
const ProfileCard = observer(() => {
const { ui } = useStore()
- const { mutate } = useProfileUpdateMutation()
- const user = ui.profile
+ const { mutateAsync } = useProfileUpdateMutation()
+
+ const { data: profile } = useProfileQuery()
+ // TODO: ^ handle loading state
const updateUser = async (model: any) => {
try {
- mutate({
+ await mutateAsync({
firstName: model.first_name,
lastName: model.last_name,
})
@@ -58,26 +59,10 @@ const ProfileCard = observer(() => {
}
}
- const [session, setSession] = useState(null)
-
- useEffect(() => {
- let cancel = false
- ;(async () => {
- const {
- data: { session },
- } = await auth.getSession()
- if (session && !cancel) setSession(session)
- })()
-
- return () => {
- cancel = true
- }
- }, [])
-
return (
@@ -93,8 +78,8 @@ const ProfileCard = observer(() => {
},
}}
model={{
- first_name: user?.first_name ?? '',
- last_name: user?.last_name ?? '',
+ first_name: profile?.first_name ?? '',
+ last_name: profile?.last_name ?? '',
}}
onSubmit={updateUser}
/>
@@ -107,8 +92,8 @@ const ProfileCard = observer(() => {
)
})
-const Profile = observer(({ session }: any) => {
- const { ui } = useStore()
+const Profile = ({ profile }: { profile?: ProfileType }) => {
+ const session = useSession()
return (
{
disabled
label="Username"
layout="horizontal"
- value={ui.profile?.username ?? ''}
+ value={profile?.username ?? ''}
/>
{session?.user.app_metadata.provider === 'email' && (
@@ -154,7 +139,7 @@ const Profile = observer(({ session }: any) => {
)
-})
+}
const ThemeSettings = observer(() => {
const { ui } = useStore()
diff --git a/studio/pages/project/[ref]/reports/index.tsx b/studio/pages/project/[ref]/reports/index.tsx
index f4ae714fbba..aece220eb06 100644
--- a/studio/pages/project/[ref]/reports/index.tsx
+++ b/studio/pages/project/[ref]/reports/index.tsx
@@ -4,10 +4,11 @@ import { useRouter } from 'next/router'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { NextPageWithLayout } from 'types'
-import { checkPermissions, useFlag, useStore } from 'hooks'
+import { checkPermissions, useFlag, useParams, useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { useProjectContentStore } from 'stores/projectContentStore'
+import { useProfileQuery } from 'data/profile/profile-query'
import Loading from 'components/ui/Loading'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import { createReport } from 'components/to-be-cleaned/Reports/Reports.utils'
@@ -17,15 +18,16 @@ export const UserReportPage: NextPageWithLayout = () => {
const [loading, setLoading] = useState(true)
const router = useRouter()
- const { ref } = router.query
+ const { ref } = useParams()
+ const { data: profile } = useProfileQuery()
const { ui } = useStore()
const project = ui.selectedProject
const contentStore = useProjectContentStore(ref)
const canCreateReport = checkPermissions(PermissionAction.CREATE, 'user_content', {
- resource: { type: 'report', owner_id: ui.profile?.id },
- subject: { id: ui.profile?.id },
+ resource: { type: 'report', owner_id: profile?.id },
+ subject: { id: profile?.id },
})
const kpsEnabled = useFlag('initWithKps')
diff --git a/studio/stores/UiStore.ts b/studio/stores/UiStore.ts
index 74202e1623e..b568fcc85ab 100644
--- a/studio/stores/UiStore.ts
+++ b/studio/stores/UiStore.ts
@@ -15,7 +15,6 @@ export interface IUiStore {
selectedProjectBaseInfo?: ProjectBase
selectedOrganization?: Organization
notification?: Notification
- profile?: User
permissions?: Permission[]
googleAnalyticsProps?: GoogleAnalyticsProps
@@ -26,7 +25,7 @@ export interface IUiStore {
setProjectRef: (ref?: string) => void
setOrganizationSlug: (slug?: string) => void
setNotification: (notification: Notification) => string
- setProfile: (value?: User) => void
+ setProfile: (value: User) => void
setPermissions: (permissions?: Permission[]) => void
setGaClientId: (clientId?: string) => void
}
@@ -39,7 +38,6 @@ export default class UiStore implements IUiStore {
selectedProjectRef?: string
selectedOrganizationSlug?: string
notification?: Notification
- profile?: User
permissions?: Permission[] = []
constructor(rootStore: IRootStore) {
@@ -158,12 +156,8 @@ export default class UiStore implements IUiStore {
return id
}
- setProfile(value?: User) {
- if (value && value?.id !== this.profile?.id) {
- Telemetry.sendIdentify(value, this.googleAnalyticsProps)
- }
-
- this.profile = value
+ setProfile(value: User) {
+ Telemetry.sendIdentify(value, this.googleAnalyticsProps)
}
setPermissions(permissions?: any) {
diff --git a/studio/types/next.ts b/studio/types/next.ts
index 04bd6659eb4..629d626cc8b 100644
--- a/studio/types/next.ts
+++ b/studio/types/next.ts
@@ -1,6 +1,6 @@
import { NextPage } from 'next'
import { AppProps } from 'next/app'
-import { ReactElement, ReactNode } from 'react'
+import { ComponentType, ReactElement, ReactNode } from 'react'
export type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
@@ -9,3 +9,9 @@ export type AppPropsWithLayout = AppProps & {
export type NextPageWithLayout
= NextPage
& {
getLayout?: (page: ReactElement) => ReactNode
}
+
+export function isNextPageWithLayout(
+ Component: ComponentType | NextPageWithLayout
+): Component is NextPageWithLayout {
+ return 'getLayout' in Component && typeof Component.getLayout === 'function'
+}