Update parts of builder with core API refactor

This commit is contained in:
Andrew Kingston 2022-01-20 18:42:30 +00:00
parent b74815d41b
commit 6a7aed1c0d
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 IntercomClient from "./IntercomClient"
import SentryClient from "./SentryClient"
import { Events } from "./constants"
import { notifications } from "@budibase/bbui"
const posthog = new PosthogClient(
process.env.POSTHOG_TOKEN,
@ -17,13 +18,15 @@ class AnalyticsHub {
}
async activate() {
const analyticsStatus = await api.get("/api/analytics")
const json = await analyticsStatus.json()
// Analytics disabled
if (!json.enabled) return
this.clients.forEach(client => client.init())
try {
// Check analytics are enabled
const analyticsStatus = await API.getAnalyticsStatus()
if (analyticsStatus.enabled) {
this.clients.forEach(client => client.init())
}
} catch (error) {
notifications.error("Error checking analytics status")
}
}
identify(id, metadata) {

View File

@ -1,40 +1,44 @@
import {
createAPIClient,
CookieUtils,
Constants,
} from "@budibase/frontend-core"
import { store } from "./index"
import { get as svelteGet } from "svelte/store"
import { CookieUtils, Constants } from "@budibase/frontend-core"
import { get } from "svelte/store"
import { notifications } from "@budibase/bbui"
const apiCall =
method =>
async (url, body, headers = { "Content-Type": "application/json" }) => {
headers["x-budibase-app-id"] = svelteGet(store).appId
const json = headers["Content-Type"] === "application/json"
const resp = await fetch(url, {
method: method,
body: json ? JSON.stringify(body) : body,
headers,
})
if (resp.status === 403) {
export const API = createAPIClient({
attachHeaders: headers => {
// Attach app ID header from store
headers["x-budibase-app-id"] = get(store).appId
},
onError: error => {
const { url, message, status } = error
// Log all API errors to Sentry
// 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")) {
return { json: () => [] }
return
}
// Remove the auth cookie
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")) {
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,
tables,
} from "stores/backend"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { API } from "api"
import { FrontendTypes } from "constants"
import analytics, { Events } from "analytics"
import {
@ -29,6 +28,7 @@ import {
} from "../componentUtils"
import { Helpers } from "@budibase/bbui"
import { removeBindings } from "../dataBinding"
import { notifications } from "@budibase/bbui"
const INITIAL_FRONTEND_STATE = {
apps: [],
@ -68,15 +68,12 @@ export const getFrontendStore = () => {
store.actions = {
initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg
const components = await fetchComponentLibDefinitions(application.appId)
// make sure app isn't locked
if (
components &&
components.status === 400 &&
components.message?.includes("lock")
) {
throw { ok: false, reason: "locked" }
}
// Fetch component definitions.
// Allow errors to propagate.
let components = await API.fetchComponentLibDefinitions(application.appId)
// Reset store state
store.update(state => ({
...state,
libraries: application.componentLibraries,
@ -100,56 +97,57 @@ export const getFrontendStore = () => {
version: application.version,
revertableVersion: application.revertableVersion,
}))
await hostingStore.actions.fetch()
// 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)
tables.init()
await hostingStore.actions.fetch()
await datasources.init()
await integrations.init()
await queries.init()
await tables.init()
},
theme: {
save: async theme => {
const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, { theme })
if (response.status === 200) {
const metadata = { appId, theme }
try {
await API.saveAppMetadata(metadata)
store.update(state => {
state.theme = theme
return state
})
} else {
throw new Error("Error updating theme")
} catch (error) {
notifications.error("Error updating theme")
}
},
},
customTheme: {
save: async customTheme => {
const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, {
customTheme,
})
if (response.status === 200) {
const metadata = { appId, customTheme }
try {
await API.saveAppMetadata(metadata)
store.update(state => {
state.customTheme = customTheme
return state
})
} else {
throw new Error("Error updating theme")
} catch (error) {
notifications.error("Error updating custom theme")
}
},
},
routing: {
fetch: async () => {
const response = await api.get("/api/routing")
const json = await response.json()
store.update(state => {
state.routes = json.routes
return state
})
try {
const routes = await API.getAppRoutes()
console.log(routes)
store.update(state => {
state.routes = routes.routes
return state
})
} catch (error) {
notifications.error("Error fetching app routes")
}
},
},
screens: {
@ -172,81 +170,100 @@ export const getFrontendStore = () => {
})
},
create: async screen => {
screen = await store.actions.screens.save(screen)
store.update(state => {
state.selectedScreenId = screen._id
state.selectedComponentId = screen.props._id
state.currentFrontEndType = FrontendTypes.SCREEN
selectedAccessRole.set(screen.routing.roleId)
return state
})
return screen
try {
const savedScreen = await API.saveScreen(screen)
store.update(state => {
state.selectedScreenId = savedScreen._id
state.selectedComponentId = savedScreen.props._id
state.currentFrontEndType = FrontendTypes.SCREEN
selectedAccessRole.set(savedScreen.routing.roleId)
return savedScreen
})
// Refresh routes
await store.actions.routing.fetch()
return savedScreen
} catch (error) {
notifications.error("Error creating screen")
return null
}
},
save: async screen => {
const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen)
if (response.status !== 200) {
return
}
screen = await response.json()
await store.actions.routing.fetch()
try {
const creatingNewScreen = screen._id === undefined
const savedScreen = await API.saveScreen(screen)
store.update(state => {
const idx = state.screens.findIndex(x => x._id === savedScreen._id)
if (idx !== -1) {
state.screens.splice(idx, 1, savedScreen)
} else {
state.screens.push(savedScreen)
}
return state
})
store.update(state => {
const foundScreen = state.screens.findIndex(
el => el._id === screen._id
)
if (foundScreen !== -1) {
state.screens.splice(foundScreen, 1)
// Refresh routes
await store.actions.routing.fetch()
// Select the new screen if creating a new one
if (creatingNewScreen) {
store.actions.screens.select(savedScreen._id)
}
state.screens.push(screen)
return state
})
if (creatingNewScreen) {
store.actions.screens.select(screen._id)
return savedScreen
} catch (error) {
notifications.error("Error saving screen")
return null
}
return screen
},
delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
const screenDeletePromises = []
store.update(state => {
for (let screenToDelete of screensToDelete) {
state.screens = state.screens.filter(
screen => screen._id !== screenToDelete._id
// Build array of promises to speed up bulk deletions
const promises = []
screensToDelete.forEach(screen => {
// Delete the screen
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}`
)
)
if (screenToDelete._id === state.selectedScreenId) {
)
})
try {
await Promise.all(promises)
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
}
//remove the link for this screen
screenDeletePromises.push(
store.actions.components.links.delete(
screenToDelete.routing.route,
screenToDelete.props._instanceName
)
)
}
return state
})
await Promise.all(screenDeletePromises)
return state
})
} catch (error) {
notifications.error("Error deleting screens")
}
},
},
preview: {
saveSelected: async () => {
const state = get(store)
const selectedAsset = get(currentAsset)
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
await store.actions.screens.save(selectedAsset)
return await store.actions.screens.save(selectedAsset)
} else {
await store.actions.layouts.save(selectedAsset)
return await store.actions.layouts.save(selectedAsset)
}
},
setDevice: device => {
@ -270,36 +287,28 @@ export const getFrontendStore = () => {
})
},
save: async layout => {
const layoutToSave = cloneDeep(layout)
const creatingNewLayout = layoutToSave._id === undefined
const response = await api.post(`/api/layouts`, layoutToSave)
const savedLayout = await response.json()
try {
const creatingNewLayout = layout._id === undefined
const savedLayout = await API.saveLayout(layout)
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
if (response.status !== 200) {
return
}
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)
// Select layout if creating a new one
if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id)
}
return state
})
// Select layout if creating a new one
if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id)
return savedLayout
} catch (error) {
notifications.error("Error saving layout")
return null
}
return savedLayout
},
find: layoutId => {
if (!layoutId) {
@ -308,23 +317,26 @@ export const getFrontendStore = () => {
const storeContents = get(store)
return storeContents.layouts.find(layout => layout._id === layoutId)
},
delete: async layoutToDelete => {
const response = await api.delete(
`/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}`
)
if (response.status !== 200) {
const json = await response.json()
throw new Error(json.message)
delete: async layout => {
if (!layout?._id) {
return
}
try {
await API.deleteLayout({
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: {
@ -414,16 +426,12 @@ export const getFrontendStore = () => {
componentName,
presetProps
)
if (!componentInstance) {
if (!componentInstance || !asset) {
return
}
// Find parent node to attach this component to
let parentComponent
if (!asset) {
return
}
if (selected) {
// Use current screen or layout as parent if no component is selected
const definition = store.actions.components.getDefinition(
@ -451,19 +459,24 @@ export const getFrontendStore = () => {
parentComponent._children.push(componentInstance)
// Save components and update UI
await store.actions.preview.saveSelected()
store.update(state => {
state.currentView = "component"
state.selectedComponentId = componentInstance._id
return state
})
const savedAsset = await store.actions.preview.saveSelected()
if (savedAsset) {
store.update(state => {
state.currentView = "component"
state.selectedComponentId = componentInstance._id
return state
})
// Log event
analytics.captureEvent(Events.COMPONENT.CREATED, {
name: componentInstance._component,
})
// Log event
analytics.captureEvent(Events.COMPONENT.CREATED, {
name: componentInstance._component,
})
return componentInstance
return componentInstance
} else {
notifications.error("Failed to create component")
return null
}
},
delete: async 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>
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
import api from "builderStore/api"
import { API } from "api"
import analytics, { Events } from "analytics"
import { store } from "builderStore"
@ -9,18 +9,14 @@
async function deployApp() {
try {
const response = await api.post("/api/deploy")
if (response.status !== 200) {
throw new Error(`status ${response.status}`)
} else {
analytics.captureEvent(Events.APP.PUBLISHED, {
appId: $store.appId,
})
notifications.success(`Application published successfully`)
}
} catch (err) {
analytics.captureException(err)
notifications.error(`Error publishing app: ${err}`)
await API.deployApp()
analytics.captureEvent(Events.APP.PUBLISHED, {
appId: $store.appId,
})
notifications.success("Application published successfully")
} catch (error) {
analytics.captureException(error)
notifications.error("Error publishing app")
}
}
</script>

View File

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

View File

@ -1,3 +1,20 @@
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",
replacement: path.resolve("./src/stores"),
},
{
find: "api",
replacement: path.resolve("./src/builderStore/api.js"),
},
{
find: "constants",
replacement: path.resolve("./src/constants"),

View File

@ -7,4 +7,13 @@ export const buildAnalyticsEndpoints = API => ({
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`,
})
},
/**
* 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 { buildRouteEndpoints } from "./routes"
import { buildRowEndpoints } from "./rows"
import { buildScreenEndpoints } from "./screens"
import { buildTableEndpoints } from "./tables"
import { buildViewEndpoints } from "./views"
@ -167,6 +168,7 @@ export const createAPIClient = config => {
...buildRelationshipEndpoints(API),
...buildRouteEndpoints(API),
...buildRowEndpoints(API),
...buildScreenEndpoints(API),
...buildTableEndpoints(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`,
})
},
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}`,
})
},
})