Merge pull request #14361 from supabase/hi/new-profile-apis

feat: new profile apis
This commit is contained in:
Hieu Pham authored and GitHub committed 2023-05-18 13:44:15 +07:00
commit ee4879b3d1
19 files changed
+163 -62

No files matched your search

@@ -8,9 +8,9 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { uuidv4 } from 'lib/helpers'
import { checkPermissions, useFlag, useStore } from 'hooks'
import useProfile from 'hooks/misc/useProfile'
import { createSqlSnippetSkeleton } from '../SQLEditor/SQLEditor.utils'
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
import { useProfileQuery } from 'data/profile/profile-query'
import { SqlSnippet } from 'data/content/sql-snippets-query'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { codeBlock } from 'common-tags'
@@ -25,7 +25,7 @@ const CommandMenuWrapper = observer(({ children }: PropsWithChildren<{}>) => {
const allowCMDKDataOptIn = useFlag('dashboardCmdkDataOptIn')
const isOptedInToAI = opt_in_tags?.includes('AI_SQL_GENERATOR_OPT_IN') ?? false
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
const { data: settings } = useProjectApiQuery({ projectRef: ref })
const canCreateSQLSnippet = checkPermissions(PermissionAction.CREATE, 'user_content', {
resource: { type: 'sql', owner_id: profile?.id },
@@ -6,6 +6,7 @@ import { Badge, Button, Loading, Listbox, IconUser, Modal, IconAlertCircle, Icon
import { Member } from 'types'
import { useStore } from 'hooks'
import useProfile from 'hooks/misc/useProfile'
import { useParams } from 'common/hooks'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
@@ -14,7 +15,6 @@ import MemberActions from './MemberActions'
import RolesHelperModal from './RolesHelperModal/RolesHelperModal'
import { getRolesManagementPermissions } from './TeamSettings.utils'
import { isInviteExpired, getUserDisplayName } from '../Organization.utils'
import { useProfileQuery } from 'data/profile/profile-query'
import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query'
import { useOrganizationDetailQuery } from 'data/organizations/organization-detail-query'
import { useOrganizationMemberUpdateMutation } from 'data/organizations/organization-member-update-mutation'
@@ -32,7 +32,7 @@ const MembersView = ({ searchString }: MembersViewProps) => {
const { ui } = useStore()
const { slug } = useParams()
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
const { data: detailData, isLoading: isLoadingOrgDetails } = useOrganizationDetailQuery({ slug })
const { data: rolesData, isLoading: isLoadingRoles } = useOrganizationRolesQuery({ slug })
const { mutate: updateOrganizationMember, isLoading } = useOrganizationMemberUpdateMutation({
@@ -4,10 +4,10 @@ import { Button, Input, IconSearch } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useStore } from 'hooks'
import useProfile from 'hooks/misc/useProfile'
import { useParams } from 'common/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'
@@ -19,7 +19,7 @@ const TeamSettings = () => {
const { ui } = useStore()
const { slug } = useParams()
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
const isOwner = ui.selectedOrganization?.is_owner
const { data: detailData } = useOrganizationDetailQuery({ slug })
@@ -17,11 +17,11 @@ import {
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions } from 'hooks'
import useProfile from 'hooks/misc/useProfile'
import { useParams } from 'common/hooks'
import { uuidv4 } from 'lib/helpers'
import { METRIC_CATEGORIES, METRICS, TIME_PERIODS_REPORTS } from 'lib/constants'
import { useProjectContentStore } from 'stores/projectContentStore'
import { useProfileQuery } from 'data/profile/profile-query'
import Loading from 'components/ui/Loading'
import DateRangePicker from 'components/to-be-cleaned/DateRangePicker'
import NoPermission from 'components/ui/NoPermission'
@@ -33,7 +33,7 @@ const DEFAULT_CHART_ROW_COUNT = 4
const Reports = () => {
const { id, ref } = useParams()
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
const [report, setReport] = useState<any>()
@@ -4,7 +4,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import Telemetry from 'lib/telemetry'
import { checkPermissions, useStore } from 'hooks'
import { useProfileQuery } from 'data/profile/profile-query'
import useProfile from 'hooks/misc/useProfile'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.constants'
import SQLCard from './SQLCard'
import { createSqlSnippetSkeleton } from '../SQLEditor.utils'
@@ -18,7 +18,7 @@ const SQLTemplates = observer(() => {
const { ui } = useStore()
const { ref } = useParams()
const router = useRouter()
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
const [sql, quickStart] = partition(SQL_TEMPLATES, { type: 'template' })
const telemetryProps = useTelemetryProps()
@@ -13,7 +13,7 @@ import DatePickers from './Logs.DatePickers'
import Link from 'next/link'
import React from 'react'
import { checkPermissions } from 'hooks'
import { useProfileQuery } from 'data/profile/profile-query'
import useProfile from 'hooks/misc/useProfile'
import { IS_PLATFORM } from 'lib/constants'
export interface LogsQueryPanelProps {
@@ -45,7 +45,7 @@ const LogsQueryPanel = ({
onDateChange,
warnings,
}: LogsQueryPanelProps) => {
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
const canCreateLogQuery = checkPermissions(PermissionAction.CREATE, 'user_content', {
resource: { type: 'log_sql', owner_id: profile?.id },
subject: { id: profile?.id },
@@ -19,10 +19,10 @@ import { CLIENT_LIBRARIES } from 'common/constants'
import { Project } from 'types'
import { useStore, useFlag } from 'hooks'
import useProfile from 'hooks/misc/useProfile'
import { post, get } from 'lib/common/fetch'
import { detectBrowser } from 'lib/helpers'
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
import { useProfileQuery } from 'data/profile/profile-query'
import Divider from 'components/ui/Divider'
import Connecting from 'components/ui/Loading'
@@ -76,7 +76,7 @@ const SupportForm: FC<Props> = ({ setSentCategory }) => {
}
}, [uploadedFiles])
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
const respondToEmail = profile?.primary_email ?? 'your email'
if (!isInitialized) {
@@ -15,8 +15,8 @@ import {
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { checkPermissions, useFlag, useStore } from 'hooks'
import useProfile from 'hooks/misc/useProfile'
import { IS_PLATFORM } from 'lib/constants'
import { useProfileQuery } from 'data/profile/profile-query'
import { uuidv4 } from 'lib/helpers'
import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem'
@@ -33,7 +33,7 @@ const SideBarContent = observer(() => {
const { ui } = useStore()
const { ref, id } = useParams()
const router = useRouter()
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
const [filterString, setFilterString] = useState('')
const { setPages, setIsOpen } = useCommandMenu()
const showCmdkHelper = useFlag('dashboardCmdk')
+1 -22
View File
@@ -5,24 +5,6 @@ import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { FC, useEffect } from 'react'
function sanitizePageViewRoute(_route?: string) {
// remove all fragments
const noFragments = _route?.split('#')[0]
// remove sensitive params
const paramsSplits = noFragments?.split('?')
const hasParams = paramsSplits && paramsSplits?.length > 1
if (hasParams) {
const urlParams = new URLSearchParams(paramsSplits[1])
const sensitiveKeys = [...urlParams.keys()].filter((x) => x.includes('token'))
const sensitiveParams = ['code', ...sensitiveKeys]
sensitiveParams.forEach((name) => urlParams.delete(name))
return `${paramsSplits[0]}?${urlParams?.toString()}`
}
return noFragments
}
const PageTelemetry: FC = ({ children }) => {
const router = useRouter()
const telemetryProps = useTelemetryProps()
@@ -53,11 +35,8 @@ const PageTelemetry: FC = ({ children }) => {
*
* @param route: the browser url
* */
const handlePageTelemetry = async (_route?: string) => {
const handlePageTelemetry = async (route: string) => {
if (IS_PLATFORM) {
// filter out sensitive query params
const route = sanitizePageViewRoute(_route)
/**
* Get referrer from browser
*/
@@ -0,0 +1,34 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { profileKeys } from './keys'
import { Profile } from './types'
export type ProfileResponse = Profile
export async function createProfile() {
const response = await post(`${API_URL}/profile`, {})
if (response.error) {
throw response.error
}
return response as ProfileResponse
}
type ProfileCreateData = Awaited<ReturnType<typeof createProfile>>
export const useProfileCreateMutation = ({
onSuccess,
...options
}: Omit<UseMutationOptions<ProfileCreateData, unknown, void>, 'mutationFn'> = {}) => {
const queryClient = useQueryClient()
return useMutation<ProfileCreateData, unknown, void>(() => createProfile(), {
async onSuccess(data, variables, context) {
await queryClient.invalidateQueries(profileKeys.profile())
await onSuccess?.(data, variables, context)
},
...options,
})
}
+7 -15
View File
@@ -3,25 +3,16 @@ import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { useCallback } from 'react'
import { profileKeys } from './keys'
export type Profile = {
id: number
auth0_id: string
primary_email: string
username: string
first_name: string
last_name: string
mobile: string | null
is_alpha_user: boolean
gotrue_id: string
free_project_limit: number
}
import { Profile } from './types'
export type ProfileResponse = Profile
export async function getProfile(signal?: AbortSignal) {
const response = await get(`${API_URL}/profile`, {
signal,
headers: {
Version: '2',
},
})
if (response.error) {
throw response.error
@@ -36,8 +27,8 @@ export type ProfileError = unknown
export const useProfileQuery = <TData = ProfileData>({
enabled = true,
...options
}: UseQueryOptions<ProfileData, ProfileError, TData> = {}) =>
useQuery<ProfileData, ProfileError, TData>(
}: UseQueryOptions<ProfileData, ProfileError, TData> = {}) => {
return useQuery<ProfileData, ProfileError, TData>(
profileKeys.profile(),
({ signal }) => getProfile(signal),
{
@@ -45,6 +36,7 @@ export const useProfileQuery = <TData = ProfileData>({
...options,
}
)
}
export const useProfilePrefetch = () => {
const client = useQueryClient()
@@ -1,5 +1,5 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { post } from 'lib/common/fetch'
import { patch } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { profileKeys } from './keys'
@@ -9,7 +9,7 @@ export type ProfileUpdateVariables = {
}
export async function updateProfile({ firstName, lastName }: ProfileUpdateVariables) {
const response = await post(`${API_URL}/profile/update`, {
const response = await patch(`${API_URL}/profile`, {
first_name: firstName,
last_name: lastName,
})
+12
View File
@@ -0,0 +1,12 @@
export type Profile = {
id: number
auth0_id: string
primary_email: string
username: string
first_name: string
last_name: string
mobile: string | null
is_alpha_user: boolean
gotrue_id: string
free_project_limit: number
}
+25 -1
View File
@@ -4,7 +4,31 @@ import { useState } from 'react'
let queryClient: QueryClient | undefined
export function getQueryClient() {
const _queryClient = queryClient ?? new QueryClient()
const _queryClient =
queryClient ??
new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// Don't retry on 404s
if (
typeof error === 'object' &&
error !== null &&
'code' in error &&
(error as any).code === 404
) {
return false
}
if (failureCount < 3) {
return true
}
return false
},
},
},
})
// For SSG and SSR always create a new queryClient
if (typeof window === 'undefined') return _queryClient
+41
View File
@@ -0,0 +1,41 @@
import { useRouter } from 'next/router'
import { useTelemetryProps } from 'common'
import { useProfileCreateMutation } from 'data/profile/profile-create-mutation'
import { useProfileQuery } from 'data/profile/profile-query'
import Telemetry from 'lib/telemetry'
/**
* Fetches the user's profile, creating one if it does not exist
*/
function useProfile() {
const router = useRouter()
const telemetryProps = useTelemetryProps()
const { mutate: createProfile, isLoading: isCreating } = useProfileCreateMutation({
onSuccess() {
Telemetry.sendEvent(
{ category: 'conversion', action: 'sign_up', label: '' },
telemetryProps,
router
)
},
})
const { isLoading, ...result } = useProfileQuery({
onError(err) {
// if the user does not yet exist, create a profile for them
if (typeof err === 'object' && err !== null && 'code' in err && (err as any).code === 404) {
createProfile()
}
},
})
return {
// Continue the loading state until the profile is created
isLoading: isLoading || isCreating,
...result,
}
}
export default useProfile
+18
View File
@@ -1,4 +1,5 @@
import { useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/router'
import { PropsWithChildren, useCallback, useEffect } from 'react'
import {
@@ -17,6 +18,7 @@ export const AuthContext = AuthContextInternal
export const AuthProvider = ({ children }: PropsWithChildren<{}>) => {
const { ui, app } = useStore()
const router = useRouter()
const telemetryProps = useTelemetryProps()
// Check for unverified GitHub users after a GitHub sign in
@@ -36,6 +38,22 @@ export const AuthProvider = ({ children }: PropsWithChildren<{}>) => {
handleEmailVerificationError()
}, [])
useEffect(() => {
const {
data: { subscription },
} = gotrueClient.onAuthStateChange((event) => {
if (event === 'SIGNED_IN') {
Telemetry.sendEvent(
{ category: 'account', action: 'sign_in', label: '' },
telemetryProps,
router
)
}
})
return subscription.unsubscribe
}, [])
// Track telemetry for the current user
useProfileQuery({
onSuccess(profile) {
+3 -2
View File
@@ -4,9 +4,10 @@ import { Button, IconMoon, IconSun, Input, Listbox } from 'ui'
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 { Profile as ProfileType } from 'data/profile/types'
import { useProfileUpdateMutation } from 'data/profile/profile-update-mutation'
import { useStore } from 'hooks'
import useProfile from 'hooks/misc/useProfile'
import { useSession } from 'lib/auth'
import Link from 'next/link'
import { NextPageWithLayout } from 'types'
@@ -40,7 +41,7 @@ const ProfileCard = observer(() => {
const { ui } = useStore()
const { mutateAsync } = useProfileUpdateMutation()
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
// TODO: ^ handle loading state
const updateUser = async (model: any) => {
+2 -2
View File
@@ -4,10 +4,10 @@ import { useRouter } from 'next/router'
import { Button, IconCheckSquare, Loading } from 'ui'
import { useStore } from 'hooks'
import useProfile from 'hooks/misc/useProfile'
import { useSignOut } from 'lib/auth'
import { API_URL } from 'lib/constants'
import { get, post, delete_ } from 'lib/common/fetch'
import { useProfileQuery } from 'data/profile/profile-query'
interface ITokenInfo {
organization_name?: string | undefined
@@ -24,7 +24,7 @@ const JoinOrganizationPage = () => {
const router = useRouter()
const { slug, token, name } = router.query
const { ui, app } = useStore()
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
const signOut = useSignOut()
const [isSubmitting, setIsSubmitting] = useState(false)
+2 -2
View File
@@ -5,11 +5,11 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { NextPageWithLayout } from 'types'
import { checkPermissions, useFlag, useStore } from 'hooks'
import useProfile from 'hooks/misc/useProfile'
import { useParams } from 'common/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'
@@ -21,7 +21,7 @@ export const UserReportPage: NextPageWithLayout = () => {
const router = useRouter()
const { ref } = useParams()
const { data: profile } = useProfileQuery()
const { data: profile } = useProfile()
const { ui } = useStore()
const project = ui.selectedProject