diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 366153f7b8e..971acd0c1bf 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -48,6 +48,7 @@ export const gettingstarted = { items: [ { name: 'Features', url: '/guides/getting-started/features' }, { name: 'Architecture', url: '/guides/getting-started/architecture' }, + { name: 'Local Development', url: '/guides/cli/local-development' }, { name: 'Framework Quickstarts', items: [ diff --git a/apps/docs/pages/guides/getting-started.mdx b/apps/docs/pages/guides/getting-started.mdx index 820eae29e0c..bb335915b34 100644 --- a/apps/docs/pages/guides/getting-started.mdx +++ b/apps/docs/pages/guides/getting-started.mdx @@ -218,6 +218,12 @@ export const resources = [ href: '/guides/getting-started/architecture', description: "An overview of Supabase's architecture and product principles.", }, + { + title: 'Local Development', + hasLightIcon: true, + href: '/guides/cli/local-development', + description: 'Use the Supabase CLI to develop locally and collaborate between teams.', + }, ] export const Page = ({ children }) => diff --git a/apps/www/data/Community.tsx b/apps/www/data/Community.tsx index 89013d080fe..95c35119b19 100644 --- a/apps/www/data/Community.tsx +++ b/apps/www/data/Community.tsx @@ -42,7 +42,7 @@ const data: CommunityItem[] = [ }, { title: 'Twitter', - stat: '60,000+', + stat: '64,000+', statLabel: 'Followers', img: 'twitter.png', detail: () => ( diff --git a/studio/.env b/studio/.env index e66cc5523ed..68180030ee1 100644 --- a/studio/.env +++ b/studio/.env @@ -8,6 +8,7 @@ SUPABASE_URL=http://localhost:8000 SUPABASE_PUBLIC_URL=http://localhost:8000 SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q +SENTRY_IGNORE_API_RESOLUTION_ERROR=1 # TODO: evaluate these variables at runtime instead # https://nextjs.org/docs/basic-features/environment-variables#loading-environment-variables diff --git a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx index 5a15174d37f..c973699deeb 100644 --- a/studio/components/interfaces/Home/ProjectList/ProjectList.tsx +++ b/studio/components/interfaces/Home/ProjectList/ProjectList.tsx @@ -21,7 +21,7 @@ const ProjectList: FC = ({ rewriteHref }) => { const { organizations, projects } = app const { isLoading: isLoadingProjects } = projects - const { data: allOverdueInvoices } = useOverdueInvoicesQuery() + const { data: allOverdueInvoices } = useOverdueInvoicesQuery({ enabled: IS_PLATFORM }) const isLoadingPermissions = IS_PLATFORM ? (ui?.permissions ?? []).length === 0 : false diff --git a/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx b/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx index ddee8e564a7..8413e9a37cf 100644 --- a/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx +++ b/studio/components/layouts/ProjectLayout/UpgradingState/UpgradingState.tsx @@ -1,6 +1,6 @@ import dayjs from 'dayjs' import Link from 'next/link' -import { useState, useEffect } from 'react' +import { useState } from 'react' import * as Tooltip from '@radix-ui/react-tooltip' import { Button, @@ -16,6 +16,7 @@ import { import { DatabaseUpgradeStatus } from '@supabase/shared-types/out/events' import { useParams, useStore } from 'hooks' +import { IS_PLATFORM } from 'lib/constants' import { DATABASE_UPGRADE_MESSAGES } from './UpgradingState.constants' import { useProjectUpgradingStatusQuery } from 'data/config/project-upgrade-status-query' @@ -24,10 +25,15 @@ const UpgradingState = () => { const { app, ui, meta } = useStore() const [loading, setLoading] = useState(false) const [isExpanded, setIsExpanded] = useState(false) - const { data } = useProjectUpgradingStatusQuery({ - projectRef: ref, - projectStatus: ui.selectedProject?.status, - }) + const { data } = useProjectUpgradingStatusQuery( + { + projectRef: ref, + projectStatus: ui.selectedProject?.status, + }, + { + enabled: IS_PLATFORM, + } + ) const project = ui.selectedProject const { initiated_at, status, progress, target_version, error } = @@ -51,12 +57,12 @@ const UpgradingState = () => { const message = `Upgrade information:%0A• Initiated at: ${initiated_at}%0A• Target Version: ${target_version}%0A• Error: ${error}` return ( -
+

{project?.name}

-
+
{isCompleted ? (
@@ -104,14 +110,14 @@ const UpgradingState = () => { ) : (
-
+

Upgrading in progress

-

+

Upgrades can take from a few minutes up to several hours depending on the size of your database. Your project will be offline while it is being upgraded.

@@ -124,14 +130,14 @@ const UpgradingState = () => { setIsExpanded(false)} /> ) : ( setIsExpanded(true)} /> )} @@ -155,7 +161,7 @@ const UpgradingState = () => { return (
{isCurrent ? ( -
+
{ />
) : isCompleted ? ( -
+
) : ( -
+
)}

{ {initiated_at !== undefined && ( -

+

Started on: {initiatedAtUTC} (UTC)

diff --git a/studio/components/ui/ProjectUpgradeFailedBanner.tsx b/studio/components/ui/ProjectUpgradeFailedBanner.tsx index 5e23e6634e2..19bab87189a 100644 --- a/studio/components/ui/ProjectUpgradeFailedBanner.tsx +++ b/studio/components/ui/ProjectUpgradeFailedBanner.tsx @@ -3,6 +3,7 @@ import Link from 'next/link' import { useState } from 'react' import { Alert, Button, IconX } from 'ui' import { useParams } from 'hooks' +import { IS_PLATFORM } from 'lib/constants' import { useProjectUpgradingStatusQuery } from 'data/config/project-upgrade-status-query' import { DatabaseUpgradeStatus } from '@supabase/shared-types/out/events' @@ -10,7 +11,7 @@ import { DatabaseUpgradeStatus } from '@supabase/shared-types/out/events' const ProjectUpgradeFailedBanner = () => { const { ref } = useParams() - const { data } = useProjectUpgradingStatusQuery({ projectRef: ref }) + const { data } = useProjectUpgradingStatusQuery({ projectRef: ref }, { enabled: IS_PLATFORM }) const { target_version, status, initiated_at, error } = data?.databaseUpgradeStatus ?? {} const key = `supabase-upgrade-${ref}-${initiated_at}` @@ -39,7 +40,7 @@ const ProjectUpgradeFailedBanner = () => { variant={'warning'} title={`Postgres version upgrade to ${target_version} was not successful (Initiated at ${initiatedAtUTC} UTC)`} actions={ -
+
diff --git a/studio/localStores/storageExplorer/StorageExplorerStore.js b/studio/localStores/storageExplorer/StorageExplorerStore.js index 57533c66f99..d35a985f3d4 100644 --- a/studio/localStores/storageExplorer/StorageExplorerStore.js +++ b/studio/localStores/storageExplorer/StorageExplorerStore.js @@ -1256,7 +1256,7 @@ class StorageExplorerStore { path: prefix, options, }) - if (res.error) console.error('Error at fetchFoldersByPath:', error) + if (res.error) console.error('Error at fetchFoldersByPath:', res.error) return res }) ) diff --git a/studio/pages/api/projects/[ref]/api/rest.ts b/studio/pages/api/projects/[ref]/api/rest.ts new file mode 100644 index 00000000000..d4c2a519ac3 --- /dev/null +++ b/studio/pages/api/projects/[ref]/api/rest.ts @@ -0,0 +1,38 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import apiWrapper from 'lib/api/apiWrapper' + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'GET': + return handleGet(req, res) + case 'HEAD': + return handleHead(req, res) + default: + res.setHeader('Allow', ['GET', 'HEAD']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handleGet = async (req: NextApiRequest, res: NextApiResponse) => { + const response = await fetch(`${process.env.SUPABASE_URL}/rest/v1/`, { + method: 'GET', + headers: { + apikey: process.env.SUPABASE_SERVICE_KEY!, + }, + }) + if (response.ok) { + const data = await response.json() + + return res.status(200).json(data) + } + + return res.status(500).json({ error: { message: 'Internal Server Error' } }) +} + +const handleHead = async (req: NextApiRequest, res: NextApiResponse) => { + res.status(200).end() +} diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/empty.ts b/studio/pages/api/storage/[ref]/buckets/[id]/empty.ts new file mode 100644 index 00000000000..9b162f6a637 --- /dev/null +++ b/studio/pages/api/storage/[ref]/buckets/[id]/empty.ts @@ -0,0 +1,30 @@ +import { createClient } from '@supabase/supabase-js' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req, res) + default: + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + + const { data, error } = await supabase.storage.emptyBucket(id as string) + if (error) { + return res.status(400).json({ error: { message: error.message } }) + } + + return res.status(200).json(data) +} diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/index.ts b/studio/pages/api/storage/[ref]/buckets/[id]/index.ts new file mode 100644 index 00000000000..72af258757b --- /dev/null +++ b/studio/pages/api/storage/[ref]/buckets/[id]/index.ts @@ -0,0 +1,46 @@ +import { createClient } from '@supabase/supabase-js' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'PATCH': + return handlePatch(req, res) + case 'DELETE': + return handleDelete(req, res) + default: + res.setHeader('Allow', ['PATCH', 'DELETE']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handlePatch = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { public: isPublicBucket } = req.body + + const { data, error } = await supabase.storage.updateBucket(id as string, { + public: isPublicBucket, + }) + if (error) { + return res.status(400).json({ error: { message: error.message } }) + } + + return res.status(200).json(data) +} + +const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + + const { data, error } = await supabase.storage.deleteBucket(id as string) + if (error) { + return res.status(400).json({ error: { message: error.message } }) + } + + return res.status(200).json(data) +} diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/download.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/download.ts new file mode 100644 index 00000000000..fa51a404ed0 --- /dev/null +++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/download.ts @@ -0,0 +1,34 @@ +import { createClient } from '@supabase/supabase-js' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req, res) + default: + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { path } = req.body + + const { data, error } = await supabase.storage.from(id as string).download(path) + if (error) { + return res.status(400).json({ error: { message: error.message } }) + } + + return res + .status(200) + .setHeader('Content-Type', 'application/octet-stream') + .send(Buffer.from(await data.arrayBuffer())) +} diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/index.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/index.ts new file mode 100644 index 00000000000..d80425dd173 --- /dev/null +++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/index.ts @@ -0,0 +1,31 @@ +import { createClient } from '@supabase/supabase-js' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'DELETE': + return handleDelete(req, res) + default: + res.setHeader('Allow', ['DELETE']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { paths } = req.body + + const { data, error } = await supabase.storage.from(id as string).remove(paths as string[]) + if (error) { + return res.status(400).json({ error: { message: error.message } }) + } + + return res.status(200).json(data) +} diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/list.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/list.ts new file mode 100644 index 00000000000..d4dc551668b --- /dev/null +++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/list.ts @@ -0,0 +1,32 @@ +import { createClient } from '@supabase/supabase-js' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req, res) + + default: + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { path, ...params } = req.body + + const { data, error } = await supabase.storage.from(id as string).list(path, params) + if (error) { + return res.status(500).json({ error: error.message }) + } + + return res.status(200).json(data) +} diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/move.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/move.ts new file mode 100644 index 00000000000..d4c8b5485f0 --- /dev/null +++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/move.ts @@ -0,0 +1,31 @@ +import { createClient } from '@supabase/supabase-js' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req, res) + default: + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { from, to } = req.body + + const { data, error } = await supabase.storage.from(id as string).move(from, to) + if (error) { + return res.status(400).json({ error: { message: error.message } }) + } + + return res.status(200).json(data) +} diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/public-url.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/public-url.ts new file mode 100644 index 00000000000..41efb4e8107 --- /dev/null +++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/public-url.ts @@ -0,0 +1,28 @@ +import { createClient } from '@supabase/supabase-js' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req, res) + default: + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { path } = req.body + + const { data } = supabase.storage.from(id as string).getPublicUrl(path) + + return res.status(200).json(data) +} diff --git a/studio/pages/api/storage/[ref]/buckets/[id]/objects/sign.ts b/studio/pages/api/storage/[ref]/buckets/[id]/objects/sign.ts new file mode 100644 index 00000000000..8677334f21b --- /dev/null +++ b/studio/pages/api/storage/[ref]/buckets/[id]/objects/sign.ts @@ -0,0 +1,31 @@ +import { createClient } from '@supabase/supabase-js' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req, res) + default: + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { path, expiresIn = 60 * 60 * 24 } = req.body + + const { data, error } = await supabase.storage.from(id as string).createSignedUrl(path, expiresIn) + if (error) { + return res.status(400).json({ error: { message: error.message } }) + } + + return res.status(200).json(data) +} diff --git a/studio/pages/api/storage/[ref]/buckets/index.ts b/studio/pages/api/storage/[ref]/buckets/index.ts new file mode 100644 index 00000000000..e426db1810c --- /dev/null +++ b/studio/pages/api/storage/[ref]/buckets/index.ts @@ -0,0 +1,42 @@ +import { createClient } from '@supabase/supabase-js' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'GET': + return handleGet(req, res) + case 'POST': + return handlePost(req, res) + + default: + res.setHeader('Allow', ['GET', 'POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handleGet = async (req: NextApiRequest, res: NextApiResponse) => { + const { data, error } = await supabase.storage.listBuckets() + if (error) { + return res.status(500).json({ error: { message: 'Internal Server Error' } }) + } + + return res.status(200).json(data) +} + +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { id, public: isPublicBucket } = req.body + + const { data, error } = await supabase.storage.createBucket(id, { public: isPublicBucket }) + if (error) { + return res.status(400).json({ error: { message: error.message } }) + } + + return res.status(200).json(data) +} diff --git a/studio/pages/project/[ref]/index.tsx b/studio/pages/project/[ref]/index.tsx index 38d0c25de3e..b6bd9ded124 100644 --- a/studio/pages/project/[ref]/index.tsx +++ b/studio/pages/project/[ref]/index.tsx @@ -41,26 +41,30 @@ const Home: NextPageWithLayout = () => { {IS_PLATFORM && project?.status !== PROJECT_STATUS.INACTIVE && }
-
-
-

Client libraries

-
-
- {CLIENT_LIBRARIES.map((library) => ( - - ))} -
-
-
-
-

Example projects

-
-
- {EXAMPLE_PROJECTS.sort((a, b) => a.title.localeCompare(b.title)).map((project) => ( - - ))} -
-
+ {project?.status !== PROJECT_STATUS.INACTIVE && ( + <> +
+
+

Client libraries

+
+
+ {CLIENT_LIBRARIES.map((library) => ( + + ))} +
+
+
+
+

Example projects

+
+
+ {EXAMPLE_PROJECTS.sort((a, b) => a.title.localeCompare(b.title)).map((project) => ( + + ))} +
+
+ + )}
) } diff --git a/studio/stores/common/PostgresMetaInterface.ts b/studio/stores/common/PostgresMetaInterface.ts index bd51fd319d5..c11e782498a 100644 --- a/studio/stores/common/PostgresMetaInterface.ts +++ b/studio/stores/common/PostgresMetaInterface.ts @@ -47,6 +47,7 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface< state = this.STATES.INITIAL data: { [key in DataKeys]: T } = {} headers: any = {} + isInitialized: boolean = false constructor( rootStore: IRootStore, @@ -68,6 +69,7 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface< count: computed, hasError: computed, isLoading: computed, + isInitialized: observable, load: action, create: action, update: action, @@ -90,10 +92,6 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface< return this.state === this.STATES.INITIAL || this.state === this.STATES.LOADING } - get isInitialized() { - return this.state === this.STATES.LOADED || this.state === this.STATES.ERROR - } - async fetchData() { const headers = { 'Content-Type': 'application/json', ...this.headers } const response = await get(this.url, { headers }) @@ -114,6 +112,8 @@ export default class PostgresMetaInterface implements IPostgresMetaInterface< console.error('Load error message', e.message) this.setError(e) this.setState(ERROR) + } finally { + if (!this.isInitialized) this.isInitialized = true } }