Update parts of builder with core API refactor

This commit is contained in:
Andrew Kingston 2022-01-20 18:42:30 +00:00
parent 55f07340e9
commit 2e73a693db
16 changed files with 345 additions and 245 deletions

View File

@ -1,8 +1,9 @@
import api from "builderStore/api" import { API } from "api"
import PosthogClient from "./PosthogClient" import PosthogClient from "./PosthogClient"
import IntercomClient from "./IntercomClient" import IntercomClient from "./IntercomClient"
import SentryClient from "./SentryClient" import SentryClient from "./SentryClient"
import { Events } from "./constants" import { Events } from "./constants"
import { notifications } from "@budibase/bbui"
const posthog = new PosthogClient( const posthog = new PosthogClient(
process.env.POSTHOG_TOKEN, process.env.POSTHOG_TOKEN,
@ -17,13 +18,15 @@ class AnalyticsHub {
} }
async activate() { async activate() {
const analyticsStatus = await api.get("/api/analytics") try {
const json = await analyticsStatus.json() // Check analytics are enabled
const analyticsStatus = await API.getAnalyticsStatus()
// Analytics disabled if (analyticsStatus.enabled) {
if (!json.enabled) return this.clients.forEach(client => client.init())
}
this.clients.forEach(client => client.init()) } catch (error) {
notifications.error("Error checking analytics status")
}
} }
identify(id, metadata) { identify(id, metadata) {

View File

@ -1,40 +1,44 @@
import {
createAPIClient,
CookieUtils,
Constants,
} from "@budibase/frontend-core"
import { store } from "./index" import { store } from "./index"
import { get as svelteGet } from "svelte/store" import { get } from "svelte/store"
import { CookieUtils, Constants } from "@budibase/frontend-core" import { notifications } from "@budibase/bbui"
const apiCall = export const API = createAPIClient({
method => attachHeaders: headers => {
async (url, body, headers = { "Content-Type": "application/json" }) => { // Attach app ID header from store
headers["x-budibase-app-id"] = svelteGet(store).appId headers["x-budibase-app-id"] = get(store).appId
const json = headers["Content-Type"] === "application/json" },
const resp = await fetch(url, {
method: method, onError: error => {
body: json ? JSON.stringify(body) : body, const { url, message, status } = error
headers,
}) // Log all API errors to Sentry
if (resp.status === 403) { // analytics.captureException(error)
// Show a notification for any errors
if (message) {
notifications.error(`Error fetching ${url}: ${message}`)
}
// Logout on 403's
if (status === 403) {
// Don't do anything if fetching templates.
// TODO: clarify why this is here
if (url.includes("/api/templates")) { if (url.includes("/api/templates")) {
return { json: () => [] } return
} }
// Remove the auth cookie
CookieUtils.removeCookie(Constants.Cookies.Auth) CookieUtils.removeCookie(Constants.Cookies.Auth)
// reload after removing cookie, go to login
// Reload after removing cookie, go to login
if (!url.includes("self") && !url.includes("login")) { if (!url.includes("self") && !url.includes("login")) {
location.reload() location.reload()
} }
} }
return resp },
} })
export const post = apiCall("POST")
export const get = apiCall("GET")
export const patch = apiCall("PATCH")
export const del = apiCall("DELETE")
export const put = apiCall("PUT")
export default {
post: apiCall("POST"),
get: apiCall("GET"),
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
put: apiCall("PUT"),
}

View File

@ -1,16 +0,0 @@
import { get } from "builderStore/api"
/**
* Fetches the definitions for component library components. This includes
* their props and other metadata from components.json.
* @param {string} appId - ID of the currently running app
*/
export const fetchComponentLibDefinitions = async appId => {
const LIB_DEFINITION_URL = `/api/${appId}/components/definitions`
try {
const libDefinitionResponse = await get(LIB_DEFINITION_URL)
return await libDefinitionResponse.json()
} catch (err) {
console.error(`Error fetching component definitions for ${appId}`, err)
}
}

View File

@ -15,8 +15,7 @@ import {
database, database,
tables, tables,
} from "stores/backend" } from "stores/backend"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries" import { API } from "api"
import api from "../api"
import { FrontendTypes } from "constants" import { FrontendTypes } from "constants"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import { import {
@ -29,6 +28,7 @@ import {
} from "../componentUtils" } from "../componentUtils"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { removeBindings } from "../dataBinding" import { removeBindings } from "../dataBinding"
import { notifications } from "@budibase/bbui"
const INITIAL_FRONTEND_STATE = { const INITIAL_FRONTEND_STATE = {
apps: [], apps: [],
@ -68,15 +68,12 @@ export const getFrontendStore = () => {
store.actions = { store.actions = {
initialise: async pkg => { initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg const { layouts, screens, application, clientLibPath } = pkg
const components = await fetchComponentLibDefinitions(application.appId)
// make sure app isn't locked // Fetch component definitions.
if ( // Allow errors to propagate.
components && let components = await API.fetchComponentLibDefinitions(application.appId)
components.status === 400 &&
components.message?.includes("lock") // Reset store state
) {
throw { ok: false, reason: "locked" }
}
store.update(state => ({ store.update(state => ({
...state, ...state,
libraries: application.componentLibraries, libraries: application.componentLibraries,
@ -100,56 +97,57 @@ export const getFrontendStore = () => {
version: application.version, version: application.version,
revertableVersion: application.revertableVersion, revertableVersion: application.revertableVersion,
})) }))
await hostingStore.actions.fetch()
// Initialise backend stores // Initialise backend stores
const [_integrations] = await Promise.all([
api.get("/api/integrations").then(r => r.json()),
])
datasources.init()
integrations.set(_integrations)
queries.init()
database.set(application.instance) database.set(application.instance)
tables.init() await hostingStore.actions.fetch()
await datasources.init()
await integrations.init()
await queries.init()
await tables.init()
}, },
theme: { theme: {
save: async theme => { save: async theme => {
const appId = get(store).appId const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, { theme }) const metadata = { appId, theme }
if (response.status === 200) { try {
await API.saveAppMetadata(metadata)
store.update(state => { store.update(state => {
state.theme = theme state.theme = theme
return state return state
}) })
} else { } catch (error) {
throw new Error("Error updating theme") notifications.error("Error updating theme")
} }
}, },
}, },
customTheme: { customTheme: {
save: async customTheme => { save: async customTheme => {
const appId = get(store).appId const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, { const metadata = { appId, customTheme }
customTheme, try {
}) await API.saveAppMetadata(metadata)
if (response.status === 200) {
store.update(state => { store.update(state => {
state.customTheme = customTheme state.customTheme = customTheme
return state return state
}) })
} else { } catch (error) {
throw new Error("Error updating theme") notifications.error("Error updating custom theme")
} }
}, },
}, },
routing: { routing: {
fetch: async () => { fetch: async () => {
const response = await api.get("/api/routing") try {
const json = await response.json() const routes = await API.getAppRoutes()
store.update(state => { console.log(routes)
state.routes = json.routes store.update(state => {
return state state.routes = routes.routes
}) return state
})
} catch (error) {
notifications.error("Error fetching app routes")
}
}, },
}, },
screens: { screens: {
@ -172,81 +170,100 @@ export const getFrontendStore = () => {
}) })
}, },
create: async screen => { create: async screen => {
screen = await store.actions.screens.save(screen) try {
store.update(state => { const savedScreen = await API.saveScreen(screen)
state.selectedScreenId = screen._id store.update(state => {
state.selectedComponentId = screen.props._id state.selectedScreenId = savedScreen._id
state.currentFrontEndType = FrontendTypes.SCREEN state.selectedComponentId = savedScreen.props._id
selectedAccessRole.set(screen.routing.roleId) state.currentFrontEndType = FrontendTypes.SCREEN
return state selectedAccessRole.set(savedScreen.routing.roleId)
}) return savedScreen
return screen })
// Refresh routes
await store.actions.routing.fetch()
return savedScreen
} catch (error) {
notifications.error("Error creating screen")
return null
}
}, },
save: async screen => { save: async screen => {
const creatingNewScreen = screen._id === undefined try {
const response = await api.post(`/api/screens`, screen) const creatingNewScreen = screen._id === undefined
if (response.status !== 200) { const savedScreen = await API.saveScreen(screen)
return store.update(state => {
} const idx = state.screens.findIndex(x => x._id === savedScreen._id)
screen = await response.json() if (idx !== -1) {
await store.actions.routing.fetch() state.screens.splice(idx, 1, savedScreen)
} else {
state.screens.push(savedScreen)
}
return state
})
store.update(state => { // Refresh routes
const foundScreen = state.screens.findIndex( await store.actions.routing.fetch()
el => el._id === screen._id
) // Select the new screen if creating a new one
if (foundScreen !== -1) { if (creatingNewScreen) {
state.screens.splice(foundScreen, 1) store.actions.screens.select(savedScreen._id)
} }
state.screens.push(screen) return savedScreen
return state } catch (error) {
}) notifications.error("Error saving screen")
return null
if (creatingNewScreen) {
store.actions.screens.select(screen._id)
} }
return screen
}, },
delete: async screens => { delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens] const screensToDelete = Array.isArray(screens) ? screens : [screens]
const screenDeletePromises = [] // Build array of promises to speed up bulk deletions
store.update(state => { const promises = []
for (let screenToDelete of screensToDelete) { screensToDelete.forEach(screen => {
state.screens = state.screens.filter( // Delete the screen
screen => screen._id !== screenToDelete._id promises.push(
API.deleteScreen({
screenId: screen._id,
screenRev: screen._rev,
})
)
// Remove links to this screen
promises.push(
store.actions.components.links.delete(
screen.routing.route,
screen.props._instanceName
) )
screenDeletePromises.push( )
api.delete( })
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
) try {
) await Promise.all(promises)
if (screenToDelete._id === state.selectedScreenId) { const deletedIds = screensToDelete.map(screen => screen._id)
store.update(state => {
// Remove deleted screens from state
state.screens = state.screens.filter(screen => {
return !deletedIds.includes(screen._id)
})
// Deselect the current screen if it was deleted
if (deletedIds.includes(state.selectedScreenId)) {
state.selectedScreenId = null state.selectedScreenId = null
} }
//remove the link for this screen return state
screenDeletePromises.push( })
store.actions.components.links.delete( } catch (error) {
screenToDelete.routing.route, notifications.error("Error deleting screens")
screenToDelete.props._instanceName }
)
)
}
return state
})
await Promise.all(screenDeletePromises)
}, },
}, },
preview: { preview: {
saveSelected: async () => { saveSelected: async () => {
const state = get(store) const state = get(store)
const selectedAsset = get(currentAsset) const selectedAsset = get(currentAsset)
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) { if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
await store.actions.screens.save(selectedAsset) return await store.actions.screens.save(selectedAsset)
} else { } else {
await store.actions.layouts.save(selectedAsset) return await store.actions.layouts.save(selectedAsset)
} }
}, },
setDevice: device => { setDevice: device => {
@ -270,36 +287,28 @@ export const getFrontendStore = () => {
}) })
}, },
save: async layout => { save: async layout => {
const layoutToSave = cloneDeep(layout) try {
const creatingNewLayout = layoutToSave._id === undefined const creatingNewLayout = layout._id === undefined
const response = await api.post(`/api/layouts`, layoutToSave) const savedLayout = await API.saveLayout(layout)
const savedLayout = await response.json() store.update(state => {
const idx = state.layouts.findIndex(x => x._id === savedLayout._id)
if (idx !== -1) {
state.layouts.splice(idx, 1, savedLayout)
} else {
state.layouts.push(savedLayout)
}
return state
})
// Abort if saving failed // Select layout if creating a new one
if (response.status !== 200) { if (creatingNewLayout) {
return store.actions.layouts.select(savedLayout._id)
}
store.update(state => {
const layoutIdx = state.layouts.findIndex(
stateLayout => stateLayout._id === savedLayout._id
)
if (layoutIdx >= 0) {
// update existing layout
state.layouts.splice(layoutIdx, 1, savedLayout)
} else {
// save new layout
state.layouts.push(savedLayout)
} }
return state return savedLayout
}) } catch (error) {
notifications.error("Error saving layout")
// Select layout if creating a new one return null
if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id)
} }
return savedLayout
}, },
find: layoutId => { find: layoutId => {
if (!layoutId) { if (!layoutId) {
@ -308,23 +317,26 @@ export const getFrontendStore = () => {
const storeContents = get(store) const storeContents = get(store)
return storeContents.layouts.find(layout => layout._id === layoutId) return storeContents.layouts.find(layout => layout._id === layoutId)
}, },
delete: async layoutToDelete => { delete: async layout => {
const response = await api.delete( if (!layout?._id) {
`/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}` return
) }
if (response.status !== 200) { try {
const json = await response.json() await API.deleteLayout({
throw new Error(json.message) layoutId: layout._id,
layoutRev: layout._rev,
})
store.update(state => {
// Select main layout if we deleted the selected layout
if (layout._id === state.selectedLayoutId) {
state.selectedLayoutId = get(mainLayout)._id
}
state.layouts = state.layouts.filter(x => x._id !== layout._id)
return state
})
} catch (error) {
notifications.error("Failed to delete layout")
} }
store.update(state => {
state.layouts = state.layouts.filter(
layout => layout._id !== layoutToDelete._id
)
if (layoutToDelete._id === state.selectedLayoutId) {
state.selectedLayoutId = get(mainLayout)._id
}
return state
})
}, },
}, },
components: { components: {
@ -414,16 +426,12 @@ export const getFrontendStore = () => {
componentName, componentName,
presetProps presetProps
) )
if (!componentInstance) { if (!componentInstance || !asset) {
return return
} }
// Find parent node to attach this component to // Find parent node to attach this component to
let parentComponent let parentComponent
if (!asset) {
return
}
if (selected) { if (selected) {
// Use current screen or layout as parent if no component is selected // Use current screen or layout as parent if no component is selected
const definition = store.actions.components.getDefinition( const definition = store.actions.components.getDefinition(
@ -451,19 +459,24 @@ export const getFrontendStore = () => {
parentComponent._children.push(componentInstance) parentComponent._children.push(componentInstance)
// Save components and update UI // Save components and update UI
await store.actions.preview.saveSelected() const savedAsset = await store.actions.preview.saveSelected()
store.update(state => { if (savedAsset) {
state.currentView = "component" store.update(state => {
state.selectedComponentId = componentInstance._id state.currentView = "component"
return state state.selectedComponentId = componentInstance._id
}) return state
})
// Log event // Log event
analytics.captureEvent(Events.COMPONENT.CREATED, { analytics.captureEvent(Events.COMPONENT.CREATED, {
name: componentInstance._component, name: componentInstance._component,
}) })
return componentInstance return componentInstance
} else {
notifications.error("Failed to create component")
return null
}
}, },
delete: async component => { delete: async component => {
if (!component) { if (!component) {

View File

@ -1,23 +0,0 @@
import { writable } from "svelte/store"
import { generate } from "shortid"
export const notificationStore = writable({
notifications: [],
})
export function send(message, type = "default") {
notificationStore.update(state => {
state.notifications = [
...state.notifications,
{ id: generate(), type, message },
]
return state
})
}
export const notifier = {
danger: msg => send(msg, "danger"),
warning: msg => send(msg, "warning"),
info: msg => send(msg, "info"),
success: msg => send(msg, "success"),
}

View File

@ -1,6 +1,6 @@
<script> <script>
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui" import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
import api from "builderStore/api" import { API } from "api"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import { store } from "builderStore" import { store } from "builderStore"
@ -9,18 +9,14 @@
async function deployApp() { async function deployApp() {
try { try {
const response = await api.post("/api/deploy") await API.deployApp()
if (response.status !== 200) { analytics.captureEvent(Events.APP.PUBLISHED, {
throw new Error(`status ${response.status}`) appId: $store.appId,
} else { })
analytics.captureEvent(Events.APP.PUBLISHED, { notifications.success("Application published successfully")
appId: $store.appId, } catch (error) {
}) analytics.captureException(error)
notifications.success(`Application published successfully`) notifications.error("Error publishing app")
}
} catch (err) {
analytics.captureException(err)
notifications.error(`Error publishing app: ${err}`)
} }
} }
</script> </script>

View File

@ -3,7 +3,7 @@
// This will eventually be replaced by the new client implementation when we // This will eventually be replaced by the new client implementation when we
// add a core package. // add a core package.
import { writable, derived, get } from "svelte/store" import { writable, derived, get } from "svelte/store"
import * as API from "../builderStore/api" import { API } from "api"
import { LuceneUtils } from "@budibase/frontend-core" import { LuceneUtils } from "@budibase/frontend-core"
const defaultOptions = { const defaultOptions = {
@ -50,7 +50,7 @@ export const fetchTableData = opts => {
const fetchPage = async bookmark => { const fetchPage = async bookmark => {
lastBookmark = bookmark lastBookmark = bookmark
const { tableId, limit, sortColumn, sortOrder, paginate } = options const { tableId, limit, sortColumn, sortOrder, paginate } = options
const res = await API.post(`/api/${options.tableId}/search`, { return await API.searchTable({
tableId, tableId,
query, query,
limit, limit,
@ -60,7 +60,6 @@ export const fetchTableData = opts => {
paginate, paginate,
bookmark, bookmark,
}) })
return await res.json()
} }
// Fetches a fresh set of results from the server // Fetches a fresh set of results from the server
@ -77,7 +76,7 @@ export const fetchTableData = opts => {
// are objects // are objects
let enrichedSchema = schema let enrichedSchema = schema
if (!enrichedSchema) { if (!enrichedSchema) {
const definition = await API.get(`/api/tables/${tableId}`) const definition = await API.fetchTableDefinition(tableId)
enrichedSchema = definition?.schema ?? null enrichedSchema = definition?.schema ?? null
} }
if (enrichedSchema) { if (enrichedSchema) {

View File

@ -1,3 +1,20 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { API } from "api"
export const integrations = writable({}) const createIntegrationsStore = () => {
const store = writable(null)
return {
...store,
init: async () => {
try {
const integrations = await API.getIntegrations()
store.set(integrations)
} catch (error) {
store.set(null)
}
},
}
}
export const integrations = createIntegrationsStore()

View File

@ -56,6 +56,10 @@ export default ({ mode }) => {
find: "stores", find: "stores",
replacement: path.resolve("./src/stores"), replacement: path.resolve("./src/stores"),
}, },
{
find: "api",
replacement: path.resolve("./src/builderStore/api.js"),
},
{ {
find: "constants", find: "constants",
replacement: path.resolve("./src/constants"), replacement: path.resolve("./src/constants"),

View File

@ -7,4 +7,13 @@ export const buildAnalyticsEndpoints = API => ({
url: `/api/analytics/ping`, url: `/api/analytics/ping`,
}) })
}, },
/**
* Gets the current status of analytics for this environment
*/
getAnalyticsStatus: async () => {
return await API.get({
url: "/api/analytics",
})
},
}) })

View File

@ -7,4 +7,27 @@ export const buildAppEndpoints = API => ({
url: `/api/applications/${appId}/appPackage`, url: `/api/applications/${appId}/appPackage`,
}) })
}, },
/**
* Saves and patches metadata about an app
* @param metadata the app metadata to save
*/
saveAppMetadata: async metadata => {
if (!metadata?.appId) {
throw API.error("App metadata must have an appId set")
}
return await API.put({
url: `/api/applications/${metadata.appId}`,
body: metadata,
})
},
/**
* Deploys the current app.
*/
deployApp: async () => {
return await API.post({
url: "/api/deploy",
})
},
}) })

View File

@ -0,0 +1,17 @@
export const buildBuilderEndpoints = API => ({
/**
* Fetches the definitions for component library components. This includes
* their props and other metadata from components.json.
* @param {string} appId - ID of the currently running app
*/
fetchComponentLibDefinitions: async appId => {
return await API.get({ url: `/api/${appId}/components/definitions` })
},
/**
* Gets the list of available integrations.
*/
getIntegrations: async () => {
return await API.get({ url: "/api/integrations" })
},
})

View File

@ -8,6 +8,7 @@ import { buildQueryEndpoints } from "./queries"
import { buildRelationshipEndpoints } from "./relationships" import { buildRelationshipEndpoints } from "./relationships"
import { buildRouteEndpoints } from "./routes" import { buildRouteEndpoints } from "./routes"
import { buildRowEndpoints } from "./rows" import { buildRowEndpoints } from "./rows"
import { buildScreenEndpoints } from "./screens"
import { buildTableEndpoints } from "./tables" import { buildTableEndpoints } from "./tables"
import { buildViewEndpoints } from "./views" import { buildViewEndpoints } from "./views"
@ -167,6 +168,7 @@ export const createAPIClient = config => {
...buildRelationshipEndpoints(API), ...buildRelationshipEndpoints(API),
...buildRouteEndpoints(API), ...buildRouteEndpoints(API),
...buildRowEndpoints(API), ...buildRowEndpoints(API),
...buildScreenEndpoints(API),
...buildTableEndpoints(API), ...buildTableEndpoints(API),
...buildViewEndpoints(API), ...buildViewEndpoints(API),
} }

View File

@ -0,0 +1,23 @@
export const buildLayoutEndpoints = API => ({
/**
* Saves a layout.
* @param layout the layout to save
*/
saveLayout: async layout => {
return await API.post({
url: "/api/layouts",
body: layout,
})
},
/**
* Deletes a layout.
* @param layoutId the ID of the layout to delete
* @param layoutRev the rev of the layout to delete
*/
deleteLayout: async ({ layoutId, layoutRev }) => {
return await API.delete({
url: `/api/layouts/${layoutId}/${layoutRev}`,
})
},
})

View File

@ -7,4 +7,10 @@ export const buildRouteEndpoints = API => ({
url: `/api/routing/client`, url: `/api/routing/client`,
}) })
}, },
fetchAppRoutes: async () => {
return await API.get({
url: "/api/routing",
})
},
}) })

View File

@ -0,0 +1,23 @@
export const buildScreenEndpoints = API => ({
/**
* Saves a screen definition
* @param screen the screen to save
*/
saveScreen: async screen => {
return await API.post({
url: "/api/screens",
body: screen,
})
},
/**
* Deletes a screen.
* @param screenId the ID of the screen to delete
* @param screenRev the rev of the screen to delete
*/
deleteScreen: async ({ screenId, screenRev }) => {
return await API.delete({
url: `/api/screens/${screenId}/${screenRev}`,
})
},
})