From d4967a84ea6c6c86cb7ddec207f652801de66b60 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Mon, 3 Oct 2022 23:17:35 +1100 Subject: [PATCH] chore: reuse MetaStores instead of recreating on projectRef change --- .../NavigationBar/NavigationBar.tsx | 4 +- studio/hooks/misc/withAuth.tsx | 4 +- studio/stores/RootStore.ts | 77 +++++++-------- studio/stores/UiStore.ts | 2 +- studio/stores/app/ProjectStore.ts | 6 +- studio/stores/common/PostgresMetaInterface.ts | 20 +++- studio/stores/pgmeta/MetaStore.ts | 94 +++++++++++++++---- studio/stores/pgmeta/OpenApiStore.ts | 16 ++++ 8 files changed, 154 insertions(+), 69 deletions(-) diff --git a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index 6c0d846faa5..c7b1b868dd9 100644 --- a/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -14,8 +14,8 @@ interface Props {} const NavigationBar: FC = ({}) => { const router = useRouter() - const { ui } = useStore() - const projectRef = ui.selectedProjectRef as string + const { selectedProjectRef, ui } = useStore() + const projectRef = selectedProjectRef ?? '[ref]' const projectBaseInfo = ui.selectedProjectBaseInfo const ongoingIncident = useFlag('ongoingIncident') diff --git a/studio/hooks/misc/withAuth.tsx b/studio/hooks/misc/withAuth.tsx index c420febda47..f8a76c6d0cb 100644 --- a/studio/hooks/misc/withAuth.tsx +++ b/studio/hooks/misc/withAuth.tsx @@ -64,7 +64,9 @@ export function withAuth( useEffect(() => { if (!isLoading && router.isReady) { - rootStore.setProjectRef(ref ? String(ref) : undefined) + if (ref) { + rootStore.setProjectRef(Array.isArray(ref) ? ref[0] : ref) + } rootStore.setOrganizationSlug(slug ? String(slug) : undefined) } }, [isLoading, router.isReady, ref, slug]) diff --git a/studio/stores/RootStore.ts b/studio/stores/RootStore.ts index de105b7e1fb..eadb3754d51 100644 --- a/studio/stores/RootStore.ts +++ b/studio/stores/RootStore.ts @@ -24,7 +24,10 @@ export interface IRootStore { functions: IProjectFunctionsStore backups: IProjectBackupsStore authConfig: IProjectAuthConfigStore - setProjectRef: (value?: string) => void + + selectedProjectRef?: string + + setProjectRef: (value: string) => void setOrganizationSlug: (value?: string) => void } export class RootStore implements IRootStore { @@ -36,43 +39,17 @@ export class RootStore implements IRootStore { backups: IProjectBackupsStore authConfig: IProjectAuthConfigStore + selectedProjectRef: string | undefined + constructor() { this.app = new AppStore(this) this.ui = new UiStore(this) this.meta = new MetaStore(this, { projectRef: '', connectionString: '' }) - // @ts-ignore this.content = new ProjectContentStore(this, { projectRef: '' }) this.functions = new ProjectFunctionsStore(this, { projectRef: '' }) this.backups = new ProjectBackupsStore(this, { projectRef: '' }) this.authConfig = new ProjectAuthConfigStore(this, { projectRef: '' }) - - /** - * TODO: meta and content are not observable - * meaning that when meta and content object change mobx doesnt trigger new event - * - * Workaround for now - * we need to use ui.selectedProject along with meta and content - * cos whenever ui.selectedProject changes, the reaction will create new meta and content stores - */ - reaction( - () => this.ui.selectedProject, - (selectedProject) => { - if (selectedProject) { - // @ts-ignore - this.meta = new MetaStore(this, { - projectRef: selectedProject.ref, - connectionString: selectedProject.connectionString ?? '', - }) - } else { - // @ts-ignore - this.meta = new MetaStore(this, { - projectRef: '', - connectionString: '', - }) - } - } - ) } /** @@ -80,23 +57,35 @@ export class RootStore implements IRootStore { * * This method will also trigger project detail loading when it's not available */ - setProjectRef(value?: string) { - if (this.ui.selectedProject?.ref === value) return - if (value) { - // fetch project detail when - // - project not found yet. projectStore is loading - // - connectionString is not available. projectStore loaded - const found = this.app.projects.find((x: Project) => x.ref == value) - if (!found || !found.connectionString) { - this.app.projects.fetchDetail(value) - } + setProjectRef(value: string) { + if (this.selectedProjectRef === value) return + this.selectedProjectRef = value + + // reset ui projectRef in case of switching projects + // this will show the loading screen instead of showing the previous project + this.ui.setProjectRef(undefined) + + const setProjectRefs = (project: Project) => { + this.meta.setProjectDetails(project) + this.functions.setProjectRef(project.ref) + this.authConfig.setProjectRef(project.ref) + this.content.setProjectRef(project.ref) + this.backups.setProjectRef(project.ref) + // ui set must come last + this.ui.setProjectRef(project.ref) } - this.ui.setProjectRef(value) - this.functions.setProjectRef(value) - this.authConfig.setProjectRef(value) - this.content.setProjectRef(value) - this.backups.setProjectRef(value) + // fetch project detail when + // - project not found yet. projectStore is loading + // - connectionString is not available. projectStore loaded + const found = this.app.projects.find((x: Project) => x.ref === value) + if (!found || !found.connectionString) { + this.app.projects.fetchDetail(value, (project) => { + setProjectRefs(project) + }) + } else { + setProjectRefs(found) + } } setOrganizationSlug(value?: string) { diff --git a/studio/stores/UiStore.ts b/studio/stores/UiStore.ts index 3f6328beaf1..d751e77b580 100644 --- a/studio/stores/UiStore.ts +++ b/studio/stores/UiStore.ts @@ -57,7 +57,7 @@ export default class UiStore implements IUiStore { get selectedProject() { if (this.selectedProjectRef) { const found = this.rootStore.app.projects.find( - (x: Project) => x.ref == this.selectedProjectRef + (x: Project) => x.ref === this.selectedProjectRef ) return !!found?.connectionString ? found : undefined } diff --git a/studio/stores/app/ProjectStore.ts b/studio/stores/app/ProjectStore.ts index fa7a47e021f..77236369577 100644 --- a/studio/stores/app/ProjectStore.ts +++ b/studio/stores/app/ProjectStore.ts @@ -9,7 +9,7 @@ import PostgresMetaInterface, { IPostgresMetaInterface } from '../common/Postgre import pingPostgrest from 'lib/pingPostgrest' export interface IProjectStore extends IPostgresMetaInterface { - fetchDetail: (projectRef: string) => Promise + fetchDetail: (projectRef: string, callback?: (project: Project) => void) => Promise } export default class ProjectStore extends PostgresMetaInterface { @@ -44,7 +44,7 @@ export default class ProjectStore extends PostgresMetaInterface { this.data = formattedValue } - async fetchDetail(projectRef: string) { + async fetchDetail(projectRef: string, callback?: (project: Project) => void) { const url = `${this.url}/${projectRef}` const headers = constructHeaders(this.headers) const response = await get(url, { headers }) @@ -56,6 +56,8 @@ export default class ProjectStore extends PostgresMetaInterface { // update project detail by key id this.data[project.id] = project + callback?.(project) + // lazy fetches if (IS_PLATFORM) { this.fetchSubscriptionTier(project) diff --git a/studio/stores/common/PostgresMetaInterface.ts b/studio/stores/common/PostgresMetaInterface.ts index 343824ad984..674946802e1 100644 --- a/studio/stores/common/PostgresMetaInterface.ts +++ b/studio/stores/common/PostgresMetaInterface.ts @@ -22,6 +22,9 @@ export interface IPostgresMetaInterface { find: (filter?: any) => T | undefined byId: (id: number | string) => T | undefined initialDataArray: (value: T[]) => void + + setUrl: (url: string) => void + setHeaders: (headers: { [prop: string]: any }) => void } // [TODO] Need to refactor the logic for 'isInitialized' @@ -161,8 +164,8 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface< if (response.error) throw response.error const data = response as T - const indentity = response[this.identifier] - this.data[indentity] = data + const identity = response[this.identifier] + this.data[identity] = data return data } catch (error: any) { return { data: null, error } @@ -197,4 +200,17 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface< return { error } } } + + setUrl(url: string) { + this.url = url + + // if the url changes, we need to reset the state + this.state = this.STATES.INITIAL + this.data = {} + this.error = null + } + + setHeaders(headers: { [prop: string]: any }) { + this.headers = headers + } } diff --git a/studio/stores/pgmeta/MetaStore.ts b/studio/stores/pgmeta/MetaStore.ts index cfec8459815..62beb243a10 100644 --- a/studio/stores/pgmeta/MetaStore.ts +++ b/studio/stores/pgmeta/MetaStore.ts @@ -120,6 +120,8 @@ export interface IMetaStore { columns: ColumnField[], isRealtimeEnabled: boolean ) => any + + setProjectDetails: (details: { ref: string; connectionString?: string }) => void } export default class MetaStore implements IMetaStore { rootStore: IRootStore @@ -140,6 +142,7 @@ export default class MetaStore implements IMetaStore { projectRef?: string connectionString?: string baseUrl: string + headers: { [prop: string]: any } // [Joshen] I'm going to treat this as a list of system schemas excludedSchemas = [ @@ -158,33 +161,45 @@ export default class MetaStore implements IMetaStore { 'graphql_public', ] - constructor(rootStore: IRootStore, options: { projectRef: string; connectionString: string }) { + constructor(rootStore: IRootStore, options: { projectRef: string; connectionString?: string }) { const { projectRef, connectionString } = options this.rootStore = rootStore this.projectRef = projectRef this.baseUrl = `${API_URL}/pg-meta/${projectRef}` - const headers: any = {} + this.headers = {} if (IS_PLATFORM && connectionString) { this.connectionString = connectionString - headers['x-connection-encrypted'] = connectionString + this.headers['x-connection-encrypted'] = connectionString } - this.openApi = new OpenApiStore(rootStore, `${API_URL}/props/project/${projectRef}/api`) - this.tables = new TableStore(rootStore, `${this.baseUrl}/tables`, headers) - this.columns = new ColumnStore(rootStore, `${this.baseUrl}/columns`, headers) - this.schemas = new SchemaStore(rootStore, `${this.baseUrl}/schemas`, headers) + this.openApi = new OpenApiStore( + this.rootStore, + `${API_URL}/props/project/${this.projectRef}/api` + ) + this.tables = new TableStore(this.rootStore, `${this.baseUrl}/tables`, this.headers) + this.columns = new ColumnStore(this.rootStore, `${this.baseUrl}/columns`, this.headers) + this.schemas = new SchemaStore(this.rootStore, `${this.baseUrl}/schemas`, this.headers) - this.roles = new RolesStore(rootStore, `${this.baseUrl}/roles`, headers) - this.policies = new PoliciesStore(rootStore, `${this.baseUrl}/policies`, headers) - this.hooks = new HooksStore(rootStore, `${this.baseUrl}/triggers`, headers) - this.triggers = new TriggersStore(rootStore, `${this.baseUrl}/triggers`, headers) - this.functions = new FunctionsStore(rootStore, `${this.baseUrl}/functions`, headers) - this.extensions = new ExtensionsStore(rootStore, `${this.baseUrl}/extensions`, headers, { - identifier: 'name', - }) - this.publications = new PublicationStore(rootStore, `${this.baseUrl}/publications`, headers) - this.types = new TypesStore(rootStore, `${this.baseUrl}/types`, headers) + this.roles = new RolesStore(this.rootStore, `${this.baseUrl}/roles`, this.headers) + this.policies = new PoliciesStore(this.rootStore, `${this.baseUrl}/policies`, this.headers) + this.hooks = new HooksStore(this.rootStore, `${this.baseUrl}/triggers`, this.headers) + this.triggers = new TriggersStore(this.rootStore, `${this.baseUrl}/triggers`, this.headers) + this.functions = new FunctionsStore(this.rootStore, `${this.baseUrl}/functions`, this.headers) + this.extensions = new ExtensionsStore( + this.rootStore, + `${this.baseUrl}/extensions`, + this.headers, + { + identifier: 'name', + } + ) + this.publications = new PublicationStore( + this.rootStore, + `${this.baseUrl}/publications`, + this.headers + ) + this.types = new TypesStore(this.rootStore, `${this.baseUrl}/types`, this.headers) makeObservable(this, { excludedSchemas: observable, @@ -803,4 +818,49 @@ export default class MetaStore implements IMetaStore { onProgressUpdate(insertProgress * 100) } } + + setProjectDetails({ ref, connectionString }: { ref: string; connectionString?: string }) { + this.projectRef = ref + this.baseUrl = `${API_URL}/pg-meta/${ref}` + if (IS_PLATFORM && connectionString) { + this.connectionString = connectionString + this.headers['x-connection-encrypted'] = connectionString + } + + this.openApi.setUrl(`${API_URL}/props/project/${this.projectRef}/api`) + this.openApi.setHeaders(this.headers) + + this.tables.setUrl(`${this.baseUrl}/tables`) + this.tables.setHeaders(this.headers) + + this.columns.setUrl(`${this.baseUrl}/columns`) + this.columns.setHeaders(this.headers) + + this.schemas.setUrl(`${this.baseUrl}/schemas`) + this.schemas.setHeaders(this.headers) + + this.roles.setUrl(`${this.baseUrl}/roles`) + this.roles.setHeaders(this.headers) + + this.policies.setUrl(`${this.baseUrl}/policies`) + this.policies.setHeaders(this.headers) + + this.hooks.setUrl(`${this.baseUrl}/triggers`) + this.hooks.setHeaders(this.headers) + + this.triggers.setUrl(`${this.baseUrl}/triggers`) + this.triggers.setHeaders(this.headers) + + this.functions.setUrl(`${this.baseUrl}/functions`) + this.functions.setHeaders(this.headers) + + this.extensions.setUrl(`${this.baseUrl}/extensions`) + this.extensions.setHeaders(this.headers) + + this.publications.setUrl(`${this.baseUrl}/publications`) + this.publications.setHeaders(this.headers) + + this.types.setUrl(`${this.baseUrl}/types`) + this.types.setHeaders(this.headers) + } } diff --git a/studio/stores/pgmeta/OpenApiStore.ts b/studio/stores/pgmeta/OpenApiStore.ts index aa1cbb90fab..0b8f35150d2 100644 --- a/studio/stores/pgmeta/OpenApiStore.ts +++ b/studio/stores/pgmeta/OpenApiStore.ts @@ -15,6 +15,9 @@ export interface IOpenApiStore { isLoading: boolean load: () => void + + setUrl: (url: string) => void + setHeaders: (headers: { [prop: string]: any }) => void } export default class OpenApiStore implements IOpenApiStore { STATES = { @@ -126,4 +129,17 @@ export default class OpenApiStore implements IOpenApiStore { setError(value: any) { this.error = value } + + setUrl(url: string) { + this.url = url + + // if the url changes, we need to reset the state + this.state = this.STATES.INITIAL + this.data = undefined + this.error = null + } + + setHeaders(headers: { [prop: string]: any }) { + this.headers = headers + } }