Merge pull request #6499 from supabase/hi/fix-project-detail-loading

fix: project creation flow
This commit is contained in:
Hieu Pham authored and GitHub committed 2022-04-19 12:34:12 +07:00
commit 0e0f09bd34
18 files changed
+284 -257

No files matched your search

@@ -1,11 +1,6 @@
import { DisplayApiSettings, DisplayConfigSettings } from 'components/ui/ProjectSettings'
import { FC } from 'react'
import { Typography } from '@supabase/ui'
import GetStartedPanel from './GetStartedPanel'
import {
DisplayApiSettings,
DisplayConfigSettings,
} from 'components/to-be-cleaned/DisplayProjectSettings'
interface Props {}
@@ -2,10 +2,6 @@ import Link from 'next/link'
import { FC, useEffect, useRef } from 'react'
import { observer } from 'mobx-react-lite'
import { Badge, IconArrowRight, IconLoader, Button } from '@supabase/ui'
import {
DisplayApiSettings,
DisplayConfigSettings,
} from 'components/to-be-cleaned/DisplayProjectSettings'
import ExampleProject from 'components/interfaces/Home/ExampleProject'
import ClientLibrary from 'components/interfaces/Home/ClientLibrary'
import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants'
@@ -14,6 +10,7 @@ import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { useStore } from 'hooks'
import { get } from 'lib/common/fetch'
import { Project } from 'types'
import { DisplayApiSettings, DisplayConfigSettings } from 'components/ui/ProjectSettings'
type ProjectBuildingState = { project: Project }
const ProjectBuildingState: FC<ProjectBuildingState> = ({ project }) => {
@@ -76,15 +76,14 @@ interface ContentWrapperProps {
const ContentWrapper: FC<ContentWrapperProps> = observer(({ isLoading, children }) => {
const { ui } = useStore()
const router = useRouter()
const project = ui.selectedProject
const requiresDbConnection: boolean = router.pathname !== '/project/[ref]/settings/general'
return (
<>
{isLoading || project === undefined ? (
{isLoading || ui.selectedProject === undefined ? (
<Connecting />
) : requiresDbConnection ? (
<TestConnection project={project!}>
<TestConnection project={ui.selectedProject!}>
<div className="flex flex-col flex-1 overflow-y-auto">{children}</div>
</TestConnection>
) : (
@@ -2,8 +2,8 @@ import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { FC, ReactNode } from 'react'
import { Project } from 'types'
import BuildingState from 'components/to-be-cleaned/BuildingState'
import { PROJECT_STATUS } from 'lib/constants'
import BuildingState from './BuildingState'
interface Props {
project: Project
@@ -1,230 +0,0 @@
import useSWR from 'swr'
import { FC } from 'react'
import { useRouter } from 'next/router'
import { get } from 'lib/common/fetch'
import { Badge, IconAlertCircle, Input, Loading, Typography } from '@supabase/ui'
import { JwtSecretUpdateStatus, ProjectEvents } from '@supabase/shared-types/out/events'
import { useJwtSecretUpdateStatus } from 'hooks'
import { API_URL } from 'lib/constants'
import Panel from './Panel'
const { Text, Title } = Typography
const API_SERVICE_ID = 1
export const DisplayApiSettings = () => {
const router = useRouter()
const { ref } = router.query
const { data, error }: any = useSWR(`${API_URL}/props/project/${ref}/settings`, get)
const {
isError: isJwtSecretUpdateStatusError,
isLoading: isJwtSecretUpdateStatusLoading,
jwtSecretUpdateStatus,
}: any = useJwtSecretUpdateStatus(ref)
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
if (!data || isJwtSecretUpdateStatusLoading)
return (
<ApiContentWrapper>
<Panel.Content className="py-8">
{error || isJwtSecretUpdateStatusError ? (
<div className="flex items-center space-x-2">
<Typography.Text type="secondary">
<IconAlertCircle strokeWidth={2} />
</Typography.Text>
<Typography.Text type="secondary">Failed to fetch API keys</Typography.Text>
</div>
) : (
<div className="py-4">
{/* @ts-ignore */}
<Loading active={true} />
</div>
)}
</Panel.Content>
</ApiContentWrapper>
)
// Get the API service
const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID)
const apiKeys = apiService?.service_api_keys ?? []
return (
<ApiContentWrapper>
{!data || isJwtSecretUpdateStatusLoading ? (
// @ts-ignore
<Loading active={true} />
) : (
apiKeys.map((x: any, i: number) => (
<Panel.Content
key={x.api_key}
className={
i >= 1 &&
'border-t border-panel-border-interior-light dark:border-panel-border-interior-dark'
}
>
<Input
layout="horizontal"
// @ts-ignore
label={
<>
{x.tags?.split(',').map((x: any, i: number) => (
<code key={`${x}${i}`} className="text-xs text-code">
{x}
</code>
))}
{x.tags === 'service_role' && (
<>
<code className="text-xs bg-red-900 text-white">{'secret'}</code>
</>
)}
{x.tags === 'anon' && <code className="text-xs text-code">{'public'}</code>}
</>
}
readOnly
copy={isNotUpdatingJwtSecret}
className="input-mono"
disabled
reveal={x.tags !== 'anon' && isNotUpdatingJwtSecret}
value={
jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
? 'JWT secret update failed, new API key may have issues'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
? 'Updating JWT secret...'
: x.api_key
}
onChange={() => {}}
descriptionText={
x.tags === 'service_role'
? 'This key has the ability to bypass Row Level Security. Never share it publicly.'
: 'This key is safe to use in a browser if you have enabled Row Level Security for your tables and configured policies.'
}
/>
</Panel.Content>
))
)}
</ApiContentWrapper>
)
}
export const DisplayConfigSettings = () => {
const router = useRouter()
const { ref } = router.query
const { data, error }: any = useSWR(`${API_URL}/props/project/${ref}/settings`, get)
const {
isError: isJwtSecretUpdateStatusError,
isLoading: isJwtSecretUpdateStatusLoading,
jwtSecretUpdateStatus,
}: any = useJwtSecretUpdateStatus(ref)
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
if (!data || isJwtSecretUpdateStatusLoading)
return (
<ConfigContentWrapper>
<Panel.Content className="py-8">
{error || isJwtSecretUpdateStatusError ? (
<div className="flex items-center space-x-2">
<Typography.Text type="secondary">
<IconAlertCircle strokeWidth={2} />
</Typography.Text>
<Typography.Text type="secondary">
Failed to fetch project configuration
</Typography.Text>
</div>
) : (
<div className="py-4">
{/* @ts-ignore */}
<Loading active={true} />
</div>
)}
</Panel.Content>
</ConfigContentWrapper>
)
// Get the API service
const jwtSecret = data?.project.jwt_secret ?? ''
const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID)
const apiConfig = apiService.app_config
return (
<section>
<Panel
title={
<Typography.Title level={5} className="mb-0">
Project Configuration
</Typography.Title>
}
>
<Panel.Content>
<Input
label="URL"
readOnly
copy
disabled
className="input-mono"
value={`https://${apiConfig.endpoint}`}
descriptionText="A RESTful endpoint for querying and managing your database."
layout="horizontal"
/>
</Panel.Content>
<Panel.Content className="border-t border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Input
label="JWT Secret"
readOnly
copy={isNotUpdatingJwtSecret}
reveal={isNotUpdatingJwtSecret}
disabled
value={
jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
? 'JWT secret update failed'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
? 'Updating JWT secret...'
: jwtSecret
}
className="input-mono"
descriptionText="Used to decode your JWTs. You can also use this to mint your own JWTs."
layout="horizontal"
/>
</Panel.Content>
</Panel>
</section>
)
}
const ApiContentWrapper: FC<any> = ({ children }) => {
return (
<Panel
title={
<div className="space-y-3">
<h5 className="text-base">Project API keys</h5>
<p className="text-sm text-scale-1000">
Your API is secured behind an API gateway which requires an API Key for every request.
<br />
You can use the keys below to use Supabase client libraries.
</p>
</div>
}
>
{children}
</Panel>
)
}
const ConfigContentWrapper: FC<any> = ({ children }) => {
return (
<Panel
title={
<div className="space-y-3">
<h5 className="text-base">Project Configuration</h5>
</div>
}
>
{children}
</Panel>
)
}
+1 -1
View File
@@ -22,7 +22,7 @@ function Panel(props: any) {
className={`
border border-panel-border-light dark:border-panel-border-dark
shadow-sm overflow-hidden
rounded rounded-md ${props.noMargin ? '' : 'mb-8'} ${props.className}`}
rounded-md ${props.noMargin ? '' : 'mb-8'} ${props.className}`}
>
{props.title && (
<div className={headerClasses.join(' ')}>
@@ -0,0 +1,110 @@
import { FC } from 'react'
import { useRouter } from 'next/router'
import { Input } from '@supabase/ui'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
import { useJwtSecretUpdateStatus, useProjectSettings } from 'hooks'
import { SettingsLoadingState } from './SettingsLoadingState'
import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
import Panel from 'components/to-be-cleaned/Panel'
const DisplayApiSettings = () => {
const router = useRouter()
const { ref } = router.query
const {
services,
isLoading: isProjectSettingsLoading,
isError: isProjectSettingsError,
} = useProjectSettings(ref as string | undefined)
const {
isError: isJwtSecretUpdateStatusError,
isLoading: isJwtSecretUpdateStatusLoading,
jwtSecretUpdateStatus,
}: any = useJwtSecretUpdateStatus(ref)
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
// Get the API service
const apiService = (services ?? []).find((x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID)
const apiKeys = apiService?.service_api_keys ?? []
// api keys should not be empty. However it can be populated with a delay on project creation
const isApikeysEmpty = apiKeys.length === 0
return (
<ApiContentWrapper>
{isProjectSettingsLoading || isJwtSecretUpdateStatusLoading || isApikeysEmpty ? (
<SettingsLoadingState
isError={isProjectSettingsError || isJwtSecretUpdateStatusError}
errorMessage="Failed to fetch API keys"
/>
) : (
apiKeys.map((x: any, i: number) => (
<Panel.Content
key={x.api_key}
className={
i >= 1 &&
'border-t border-panel-border-interior-light dark:border-panel-border-interior-dark'
}
>
<Input
layout="horizontal"
// @ts-ignore
label={
<>
{x.tags?.split(',').map((x: any, i: number) => (
<code key={`${x}${i}`} className="text-xs text-code">
{x}
</code>
))}
{x.tags === 'service_role' && (
<>
<code className="text-xs bg-red-900 text-white">{'secret'}</code>
</>
)}
{x.tags === 'anon' && <code className="text-xs text-code">{'public'}</code>}
</>
}
readOnly
copy={isNotUpdatingJwtSecret}
className="input-mono"
disabled
reveal={x.tags !== 'anon' && isNotUpdatingJwtSecret}
value={
jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
? 'JWT secret update failed, new API key may have issues'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
? 'Updating JWT secret...'
: x.api_key
}
onChange={() => {}}
descriptionText={
x.tags === 'service_role'
? 'This key has the ability to bypass Row Level Security. Never share it publicly.'
: 'This key is safe to use in a browser if you have enabled Row Level Security for your tables and configured policies.'
}
/>
</Panel.Content>
))
)}
</ApiContentWrapper>
)
}
export default DisplayApiSettings
const ApiContentWrapper: FC = ({ children }) => {
return (
<Panel
title={
<div className="space-y-3">
<h5 className="text-base">Project API keys</h5>
<p className="text-sm text-scale-1000">
Your API is secured behind an API gateway which requires an API Key for every request.
<br />
You can use the keys below to use Supabase client libraries.
</p>
</div>
}
>
{children}
</Panel>
)
}
@@ -0,0 +1,92 @@
import { FC } from 'react'
import { useRouter } from 'next/router'
import { Input } from '@supabase/ui'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
import { useJwtSecretUpdateStatus, useProjectSettings } from 'hooks'
import { SettingsLoadingState } from './SettingsLoadingState'
import { DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
import Panel from 'components/to-be-cleaned/Panel'
const DisplayConfigSettings = () => {
const router = useRouter()
const { ref } = router.query
const {
project,
services,
isLoading: isProjectSettingsLoading,
isError: isProjectSettingsError,
} = useProjectSettings(ref as string | undefined)
const {
isError: isJwtSecretUpdateStatusError,
isLoading: isJwtSecretUpdateStatusLoading,
jwtSecretUpdateStatus,
}: any = useJwtSecretUpdateStatus(ref)
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
// Get the API service
const jwtSecret = project?.jwt_secret ?? ''
const apiService = (services ?? []).find((x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID)
const apiConfig = apiService?.app_config ?? {}
return (
<ConfigContentWrapper>
{isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? (
<SettingsLoadingState
isError={isProjectSettingsError || isJwtSecretUpdateStatusError}
errorMessage="Failed to fetch project configuration"
/>
) : (
<>
<Panel.Content>
<Input
label="URL"
readOnly
copy
disabled
className="input-mono"
value={`https://${apiConfig.endpoint}`}
descriptionText="A RESTful endpoint for querying and managing your database."
layout="horizontal"
/>
</Panel.Content>
<Panel.Content className="border-t border-panel-border-interior-light dark:border-panel-border-interior-dark">
<Input
label="JWT Secret"
readOnly
copy={isNotUpdatingJwtSecret}
reveal={isNotUpdatingJwtSecret}
disabled
value={
jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
? 'JWT secret update failed'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
? 'Updating JWT secret...'
: jwtSecret
}
className="input-mono"
descriptionText="Used to decode your JWTs. You can also use this to mint your own JWTs."
layout="horizontal"
/>
</Panel.Content>
</>
)}
</ConfigContentWrapper>
)
}
export default DisplayConfigSettings
const ConfigContentWrapper: FC = ({ children }) => {
return (
<Panel
title={
<div className="space-y-3">
<h5 className="text-base">Project Configuration</h5>
</div>
}
>
{children}
</Panel>
)
}
@@ -0,0 +1,24 @@
import { FC } from 'react'
import { IconAlertCircle, Loading, Typography } from '@supabase/ui'
import { SettingsLoadingStateProps } from './SettingsLoadingState.types'
import Panel from 'components/to-be-cleaned/Panel'
export const SettingsLoadingState: FC<SettingsLoadingStateProps> = ({ isError, errorMessage }) => {
return (
<Panel.Content className="py-8">
{isError ? (
<div className="flex items-center space-x-2">
<Typography.Text type="secondary">
<IconAlertCircle strokeWidth={2} />
</Typography.Text>
<Typography.Text type="secondary">{errorMessage}</Typography.Text>
</div>
) : (
<div className="py-4">
{/* @ts-ignore */}
<Loading active={true} />
</div>
)}
</Panel.Content>
)
}
@@ -0,0 +1,4 @@
export interface SettingsLoadingStateProps {
isError: boolean
errorMessage: string
}
@@ -0,0 +1,4 @@
import DisplayApiSettings from './DisplayApiSettings'
import DisplayConfigSettings from './DisplayConfigSettings'
export { DisplayApiSettings, DisplayConfigSettings }
+1 -3
View File
@@ -1,10 +1,8 @@
import { useProfile, useStore } from 'hooks'
import { ComponentType, useEffect, useState } from 'react'
import { flatten, isUndefined } from 'lodash'
import { NextRouter, useRouter } from 'next/router'
import { IS_PLATFORM } from 'lib/constants'
import Connecting from 'components/ui/Loading'
import { Project } from 'types'
const PLATFORM_ONLY_PAGES = ['storage', 'reports', 'settings']
@@ -79,7 +77,7 @@ export function withAuth(
}
function defaultRedirectTo(ref: string | string[] | undefined) {
return IS_PLATFORM ? '/' : !isUndefined(ref) ? `/project/${ref}` : '/'
return IS_PLATFORM ? '/' : ref !== undefined ? `/project/${ref}` : '/'
}
function checkRedirectTo(
+15 -2
View File
@@ -1,10 +1,23 @@
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { API_URL, DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
import useSWR from 'swr'
export function useProjectSettings(ref?: string) {
const url = `${API_URL}/props/project/${ref}/settings`
const { data, error } = useSWR<any>(ref ? url : null, get)
const { data, error } = useSWR<any>(ref ? url : null, get, {
/**
* on project creation, the service_api_keys will be populated with a delay
* check for data.services.service_api_keys, return a valid refresh interval if it's empty
*/
refreshInterval: function (data: any) {
const apiService = (data?.services ?? []).find(
(x: any) => x.app.id == DEFAULT_PROJECT_API_SERVICE_ID
)
const apiKeys = apiService?.service_api_keys ?? []
const interval = apiKeys.length === 0 ? 2000 : 0
return interval
},
})
const anyError = data?.error || error
return {
+2
View File
@@ -140,3 +140,5 @@ export const PASSWORD_STRENGTH_PERCENTAGE = {
export const DEFAULT_FREE_PROJECTS_LIMIT = 2
export const STORAGE_FILE_SIZE_LIMIT_MAX_BYTES = 5 * 1024 * 1024 * 1024 // 5 GB
export const DEFAULT_PROJECT_API_SERVICE_ID = 1
+2 -3
View File
@@ -188,9 +188,8 @@ export const Wizard = observer(() => {
message: `Failed to create new project: ${response.error.message}`,
})
} else {
// Use redirect to reload store data properly
// after creating a new project
window.location.replace(`/project/${response.ref}/building`)
app.onProjectCreated(response)
router.push(`/project/${response.ref}/building`)
}
}
+3 -3
View File
@@ -5,7 +5,7 @@ import { useStore, withAuth } from 'hooks'
import { ExampleProject, ClientLibrary } from 'components/interfaces/Home'
import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants'
import { IS_PLATFORM } from 'lib/constants'
import BaseLayout from 'components/layouts'
import ProjectLayout from 'components/layouts'
import ProjectUsageSection from 'components/interfaces/Home/ProjectUsageSection'
const Home: NextPage = () => {
@@ -18,7 +18,7 @@ const Home: NextPage = () => {
: 'Welcome to your project'
return (
<BaseLayout>
<ProjectLayout>
<div className="max-w-7xl mx-auto w-full my-16 space-y-16">
<div className="mx-6 flex space-x-6 items-center">
<h1 className="text-3xl">{projectName}</h1>
@@ -45,7 +45,7 @@ const Home: NextPage = () => {
</div>
</div>
</div>
</BaseLayout>
</ProjectLayout>
)
}
+1 -1
View File
@@ -45,7 +45,7 @@ import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
import Panel from 'components/to-be-cleaned/Panel'
import MultiSelectUI from 'components/to-be-cleaned/MultiSelect'
import SchemaFormPanel from 'components/to-be-cleaned/forms/SchemaFormPanel'
import { DisplayApiSettings } from 'components/to-be-cleaned/DisplayProjectSettings'
import { DisplayApiSettings } from 'components/ui/ProjectSettings'
const JWT_SECRET_UPDATE_ERROR_MESSAGES = {
[JwtSecretUpdateError.APIServicesConfigurationUpdateFailed]:
+20
View File
@@ -12,6 +12,7 @@ export interface IAppStore {
projects: IProjectStore
organizations: OrganizationStore
database: IDatabaseStore
onProjectCreated: (project: any) => void
onProjectUpdated: (project: any) => void
onProjectDeleted: (project: any) => void
onProjectConnectionStringUpdated: (projectId: number, value: string) => void
@@ -39,6 +40,25 @@ export default class AppStore implements IAppStore {
this.database = new DatabaseStore(rootStore, `${this.baseUrl}/database`, headers)
}
onProjectCreated(project: any) {
if (project && project.id) {
const temp: Project = {
id: project.id,
ref: project.ref,
name: project.name,
status: project.status,
organization_id: project.organization_id,
cloud_provider: project.cloud_provider,
region: project.region,
inserted_at: project.inserted_at,
subscription_id: project.subscription_id,
kpsVersion: undefined,
connectionString: undefined,
}
this.projects.data[project.id] = temp
}
}
onProjectUpdated(project: any) {
if (project && project.id) {
const clone = cloneDeep(this.projects.data[project.id])