diff --git a/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx b/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx index fba67f8d2b9..2bae2c671a6 100644 --- a/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx +++ b/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx @@ -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 {} diff --git a/studio/components/to-be-cleaned/BuildingState.tsx b/studio/components/layouts/ProjectLayout/BuildingState.tsx similarity index 98% rename from studio/components/to-be-cleaned/BuildingState.tsx rename to studio/components/layouts/ProjectLayout/BuildingState.tsx index e23caf7c2af..19fe8106372 100644 --- a/studio/components/to-be-cleaned/BuildingState.tsx +++ b/studio/components/layouts/ProjectLayout/BuildingState.tsx @@ -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 = ({ project }) => { diff --git a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index be75878a99f..38de7719bf5 100644 --- a/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -76,15 +76,14 @@ interface ContentWrapperProps { const ContentWrapper: FC = 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 ? ( ) : requiresDbConnection ? ( - +
{children}
) : ( diff --git a/studio/components/layouts/ProjectLayout/TestConnection.tsx b/studio/components/layouts/ProjectLayout/TestConnection.tsx index 867d41ed7c6..7543fd5388e 100644 --- a/studio/components/layouts/ProjectLayout/TestConnection.tsx +++ b/studio/components/layouts/ProjectLayout/TestConnection.tsx @@ -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 diff --git a/studio/components/to-be-cleaned/DisplayProjectSettings.tsx b/studio/components/to-be-cleaned/DisplayProjectSettings.tsx deleted file mode 100644 index a120dc92352..00000000000 --- a/studio/components/to-be-cleaned/DisplayProjectSettings.tsx +++ /dev/null @@ -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 ( - - - {error || isJwtSecretUpdateStatusError ? ( -
- - - - Failed to fetch API keys -
- ) : ( -
- {/* @ts-ignore */} - -
- )} -
-
- ) - - // Get the API service - const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID) - const apiKeys = apiService?.service_api_keys ?? [] - - return ( - - {!data || isJwtSecretUpdateStatusLoading ? ( - // @ts-ignore - - ) : ( - apiKeys.map((x: any, i: number) => ( - = 1 && - 'border-t border-panel-border-interior-light dark:border-panel-border-interior-dark' - } - > - - {x.tags?.split(',').map((x: any, i: number) => ( - - {x} - - ))} - {x.tags === 'service_role' && ( - <> - {'secret'} - - )} - {x.tags === 'anon' && {'public'}} - - } - 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.' - } - /> - - )) - )} - - ) -} - -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 ( - - - {error || isJwtSecretUpdateStatusError ? ( -
- - - - - Failed to fetch project configuration - -
- ) : ( -
- {/* @ts-ignore */} - -
- )} -
-
- ) - - // 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 ( -
- - Project Configuration - - } - > - - - - - - - -
- ) -} - -const ApiContentWrapper: FC = ({ children }) => { - return ( - -
Project API keys
-

- Your API is secured behind an API gateway which requires an API Key for every request. -
- You can use the keys below to use Supabase client libraries. -

- - } - > - {children} -
- ) -} - -const ConfigContentWrapper: FC = ({ children }) => { - return ( - -
Project Configuration
- - } - > - {children} -
- ) -} diff --git a/studio/components/to-be-cleaned/Panel.tsx b/studio/components/to-be-cleaned/Panel.tsx index 59ffd62aa81..9ee51559ef7 100644 --- a/studio/components/to-be-cleaned/Panel.tsx +++ b/studio/components/to-be-cleaned/Panel.tsx @@ -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 && (
diff --git a/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx b/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx new file mode 100644 index 00000000000..512a986c997 --- /dev/null +++ b/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx @@ -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 ( + + {isProjectSettingsLoading || isJwtSecretUpdateStatusLoading || isApikeysEmpty ? ( + + ) : ( + apiKeys.map((x: any, i: number) => ( + = 1 && + 'border-t border-panel-border-interior-light dark:border-panel-border-interior-dark' + } + > + + {x.tags?.split(',').map((x: any, i: number) => ( + + {x} + + ))} + {x.tags === 'service_role' && ( + <> + {'secret'} + + )} + {x.tags === 'anon' && {'public'}} + + } + 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.' + } + /> + + )) + )} + + ) +} +export default DisplayApiSettings + +const ApiContentWrapper: FC = ({ children }) => { + return ( + +
Project API keys
+

+ Your API is secured behind an API gateway which requires an API Key for every request. +
+ You can use the keys below to use Supabase client libraries. +

+
+ } + > + {children} + + ) +} diff --git a/studio/components/ui/ProjectSettings/DisplayConfigSettings.tsx b/studio/components/ui/ProjectSettings/DisplayConfigSettings.tsx new file mode 100644 index 00000000000..eb099f75976 --- /dev/null +++ b/studio/components/ui/ProjectSettings/DisplayConfigSettings.tsx @@ -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 ( + + {isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? ( + + ) : ( + <> + + + + + + + + )} + + ) +} + +export default DisplayConfigSettings + +const ConfigContentWrapper: FC = ({ children }) => { + return ( + +
Project Configuration
+ + } + > + {children} +
+ ) +} diff --git a/studio/components/ui/ProjectSettings/SettingsLoadingState.tsx b/studio/components/ui/ProjectSettings/SettingsLoadingState.tsx new file mode 100644 index 00000000000..1c8dd232659 --- /dev/null +++ b/studio/components/ui/ProjectSettings/SettingsLoadingState.tsx @@ -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 = ({ isError, errorMessage }) => { + return ( + + {isError ? ( +
+ + + + {errorMessage} +
+ ) : ( +
+ {/* @ts-ignore */} + +
+ )} +
+ ) +} diff --git a/studio/components/ui/ProjectSettings/SettingsLoadingState.types.tsx b/studio/components/ui/ProjectSettings/SettingsLoadingState.types.tsx new file mode 100644 index 00000000000..b3c9248594d --- /dev/null +++ b/studio/components/ui/ProjectSettings/SettingsLoadingState.types.tsx @@ -0,0 +1,4 @@ +export interface SettingsLoadingStateProps { + isError: boolean + errorMessage: string +} diff --git a/studio/components/ui/ProjectSettings/index.tsx b/studio/components/ui/ProjectSettings/index.tsx new file mode 100644 index 00000000000..37e2dcc5b8b --- /dev/null +++ b/studio/components/ui/ProjectSettings/index.tsx @@ -0,0 +1,4 @@ +import DisplayApiSettings from './DisplayApiSettings' +import DisplayConfigSettings from './DisplayConfigSettings' + +export { DisplayApiSettings, DisplayConfigSettings } diff --git a/studio/hooks/misc/withAuth.tsx b/studio/hooks/misc/withAuth.tsx index 1ebbe3ef816..2c6e1b84f74 100644 --- a/studio/hooks/misc/withAuth.tsx +++ b/studio/hooks/misc/withAuth.tsx @@ -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( diff --git a/studio/hooks/queries/useProjectSettings.ts b/studio/hooks/queries/useProjectSettings.ts index 833bb17f489..946c4cc074d 100644 --- a/studio/hooks/queries/useProjectSettings.ts +++ b/studio/hooks/queries/useProjectSettings.ts @@ -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(ref ? url : null, get) + const { data, error } = useSWR(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 { diff --git a/studio/lib/constants/infrastructure.ts b/studio/lib/constants/infrastructure.ts index da1d6c4efaf..4dbba6f83e8 100644 --- a/studio/lib/constants/infrastructure.ts +++ b/studio/lib/constants/infrastructure.ts @@ -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 diff --git a/studio/pages/new/[slug].tsx b/studio/pages/new/[slug].tsx index 3fe60a07230..b8d9107377c 100644 --- a/studio/pages/new/[slug].tsx +++ b/studio/pages/new/[slug].tsx @@ -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`) } } diff --git a/studio/pages/project/[ref]/index.tsx b/studio/pages/project/[ref]/index.tsx index 36d861d5660..4cb7affddc4 100644 --- a/studio/pages/project/[ref]/index.tsx +++ b/studio/pages/project/[ref]/index.tsx @@ -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 ( - +

{projectName}

@@ -45,7 +45,7 @@ const Home: NextPage = () => {
-
+ ) } diff --git a/studio/pages/project/[ref]/settings/api.tsx b/studio/pages/project/[ref]/settings/api.tsx index dfe96adce81..4e83547e1ef 100644 --- a/studio/pages/project/[ref]/settings/api.tsx +++ b/studio/pages/project/[ref]/settings/api.tsx @@ -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]: diff --git a/studio/stores/app/AppStore.ts b/studio/stores/app/AppStore.ts index 63ff300c127..4e0118c4225 100644 --- a/studio/stores/app/AppStore.ts +++ b/studio/stores/app/AppStore.ts @@ -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])