diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index ce41fcc3e6..cf0d6f848c 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -7,6 +7,7 @@ on: env: POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} + INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6316bf1837..7b38a70eb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,7 @@ on: env: POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} + INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} diff --git a/lerna.json b/lerna.json index a8cda22a63..01c429c73d 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.142", + "version": "0.9.143-alpha.0", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/auth/package.json b/packages/auth/package.json index f62e4cb5e6..0457520d91 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.142", + "version": "0.9.143-alpha.0", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/auth/src/db/constants.js b/packages/auth/src/db/constants.js index 77643ce4c5..ad4f6c9f66 100644 --- a/packages/auth/src/db/constants.js +++ b/packages/auth/src/db/constants.js @@ -12,6 +12,7 @@ exports.StaticDatabases = { name: "global-info", docs: { tenants: "tenants", + usageQuota: "usage_quota", }, }, } diff --git a/packages/auth/src/db/utils.js b/packages/auth/src/db/utils.js index a1a831523e..09e2ff6314 100644 --- a/packages/auth/src/db/utils.js +++ b/packages/auth/src/db/utils.js @@ -368,8 +368,33 @@ async function getScopedConfig(db, params) { return configDoc && configDoc.config ? configDoc.config : configDoc } +function generateNewUsageQuotaDoc() { + return { + _id: StaticDatabases.PLATFORM_INFO.docs.usageQuota, + quotaReset: Date.now() + 2592000000, + usageQuota: { + automationRuns: 0, + rows: 0, + storage: 0, + apps: 0, + users: 0, + views: 0, + emails: 0, + }, + usageLimits: { + automationRuns: 1000, + rows: 4000, + apps: 4, + storage: 1000, + users: 10, + emails: 50, + }, + } +} + exports.Replication = Replication exports.getScopedConfig = getScopedConfig exports.generateConfigID = generateConfigID exports.getConfigParams = getConfigParams exports.getScopedFullConfig = getScopedFullConfig +exports.generateNewUsageQuotaDoc = generateNewUsageQuotaDoc diff --git a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js index ff38a01fbb..1ace65ba40 100644 --- a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js +++ b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js @@ -104,7 +104,7 @@ describe("third party common", () => { _id: id, email: email, } - const response = await db.post(dbUser) + const response = await db.put(dbUser) dbUser._rev = response.rev } diff --git a/packages/auth/src/middleware/passport/third-party-common.js b/packages/auth/src/middleware/passport/third-party-common.js index 7c03944232..c25aa3e0b0 100644 --- a/packages/auth/src/middleware/passport/third-party-common.js +++ b/packages/auth/src/middleware/passport/third-party-common.js @@ -71,7 +71,7 @@ exports.authenticateThirdParty = async function ( dbUser = await syncUser(dbUser, thirdPartyUser) // create or sync the user - const response = await db.post(dbUser) + const response = await db.put(dbUser) dbUser._rev = response.rev // authenticate diff --git a/packages/auth/src/security/permissions.js b/packages/auth/src/security/permissions.js index 03fa5fa562..d0308d783e 100644 --- a/packages/auth/src/security/permissions.js +++ b/packages/auth/src/security/permissions.js @@ -139,8 +139,7 @@ exports.doesHaveResourcePermission = ( // set foundSub to not subResourceId, incase there is no subResource let foundMain = false, foundSub = false - for (let [resource, level] of Object.entries(permissions)) { - const levels = getAllowedLevels(level) + for (let [resource, levels] of Object.entries(permissions)) { if (resource === resourceId && levels.indexOf(permLevel) !== -1) { foundMain = true } @@ -177,10 +176,6 @@ exports.doesHaveBasePermission = (permType, permLevel, permissionIds) => { return false } -exports.higherPermission = (perm1, perm2) => { - return levelToNumber(perm1) > levelToNumber(perm2) ? perm1 : perm2 -} - exports.isPermissionLevelHigherThanRead = level => { return levelToNumber(level) > 1 } diff --git a/packages/auth/src/security/roles.js b/packages/auth/src/security/roles.js index baa8fc40dc..71fbc10132 100644 --- a/packages/auth/src/security/roles.js +++ b/packages/auth/src/security/roles.js @@ -1,6 +1,6 @@ const { getDB } = require("../db") const { cloneDeep } = require("lodash/fp") -const { BUILTIN_PERMISSION_IDS, higherPermission } = require("./permissions") +const { BUILTIN_PERMISSION_IDS } = require("./permissions") const { generateRoleID, getRoleParams, @@ -193,8 +193,17 @@ exports.getUserPermissions = async (appId, userRoleId) => { const permissions = {} for (let role of rolesHierarchy) { if (role.permissions) { - for (let [resource, level] of Object.entries(role.permissions)) { - permissions[resource] = higherPermission(permissions[resource], level) + for (let [resource, levels] of Object.entries(role.permissions)) { + if (!permissions[resource]) { + permissions[resource] = [] + } + const permsSet = new Set(permissions[resource]) + if (Array.isArray(levels)) { + levels.forEach(level => permsSet.add(level)) + } else { + permsSet.add(levels) + } + permissions[resource] = [...permsSet] } } } diff --git a/packages/bbui/package.json b/packages/bbui/package.json index ddede26a86..42e4215caa 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "0.9.142", + "version": "0.9.143-alpha.0", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/builder/package.json b/packages/builder/package.json index 7ab313974b..2c255835ce 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.9.142", + "version": "0.9.143-alpha.0", "license": "AGPL-3.0", "private": true, "scripts": { @@ -65,10 +65,10 @@ } }, "dependencies": { - "@budibase/bbui": "^0.9.142", - "@budibase/client": "^0.9.142", + "@budibase/bbui": "^0.9.143-alpha.0", + "@budibase/client": "^0.9.143-alpha.0", "@budibase/colorpicker": "1.1.2", - "@budibase/string-templates": "^0.9.142", + "@budibase/string-templates": "^0.9.143-alpha.0", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/App.svelte b/packages/builder/src/App.svelte index 0624690b27..60051ea043 100644 --- a/packages/builder/src/App.svelte +++ b/packages/builder/src/App.svelte @@ -1,16 +1,10 @@ diff --git a/packages/builder/src/analytics.js b/packages/builder/src/analytics.js deleted file mode 100644 index 5b130a8e6b..0000000000 --- a/packages/builder/src/analytics.js +++ /dev/null @@ -1,139 +0,0 @@ -import * as Sentry from "@sentry/browser" -import posthog from "posthog-js" -import api from "builderStore/api" - -let analyticsEnabled -const posthogConfigured = process.env.POSTHOG_TOKEN && process.env.POSTHOG_URL -const sentryConfigured = process.env.SENTRY_DSN - -const FEEDBACK_SUBMITTED_KEY = "budibase:feedback_submitted" -const APP_FIRST_STARTED_KEY = "budibase:first_run" -const feedbackHours = 12 - -async function activate() { - if (analyticsEnabled === undefined) { - // only the server knows the true NODE_ENV - // this was an issue as NODE_ENV = 'cypress' on the server, - // but 'production' on the client - const response = await api.get("/api/analytics") - analyticsEnabled = (await response.json()).enabled === true - } - if (!analyticsEnabled) return - if (sentryConfigured) Sentry.init({ dsn: process.env.SENTRY_DSN }) - if (posthogConfigured) { - posthog.init(process.env.POSTHOG_TOKEN, { - autocapture: false, - capture_pageview: false, - api_host: process.env.POSTHOG_URL, - }) - posthog.set_config({ persistence: "cookie" }) - } -} - -function identify(id) { - if (!analyticsEnabled || !id) return - if (posthogConfigured) posthog.identify(id) - if (sentryConfigured) - Sentry.configureScope(scope => { - scope.setUser({ id: id }) - }) -} - -async function identifyByApiKey(apiKey) { - if (!analyticsEnabled) return true - try { - const response = await fetch( - `https://03gaine137.execute-api.eu-west-1.amazonaws.com/prod/account/id?api_key=${apiKey.trim()}` - ) - if (response.status === 200) { - const id = await response.json() - - await api.put("/api/keys/userId", { value: id }) - identify(id) - return true - } - - return false - } catch (error) { - console.log(error) - } -} - -function captureException(err) { - if (!analyticsEnabled) return - Sentry.captureException(err) - captureEvent("Error", { error: err.message ? err.message : err }) -} - -function captureEvent(eventName, props = {}) { - if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return - props.sourceApp = "builder" - posthog.capture(eventName, props) -} - -if (!localStorage.getItem(APP_FIRST_STARTED_KEY)) { - localStorage.setItem(APP_FIRST_STARTED_KEY, Date.now()) -} - -const isFeedbackTimeElapsed = sinceDateStr => { - const sinceDate = parseFloat(sinceDateStr) - const feedbackMilliseconds = feedbackHours * 60 * 60 * 1000 - return Date.now() > sinceDate + feedbackMilliseconds -} - -function submitFeedback(values) { - if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return - localStorage.setItem(FEEDBACK_SUBMITTED_KEY, Date.now()) - - const prefixedValues = Object.entries(values).reduce((obj, [key, value]) => { - obj[`feedback_${key}`] = value - return obj - }, {}) - - posthog.capture("Feedback Submitted", prefixedValues) -} - -function requestFeedbackOnDeploy() { - if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return false - const lastSubmittedStr = localStorage.getItem(FEEDBACK_SUBMITTED_KEY) - if (!lastSubmittedStr) return true - return isFeedbackTimeElapsed(lastSubmittedStr) -} - -function highlightFeedbackIcon() { - if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return false - const lastSubmittedStr = localStorage.getItem(FEEDBACK_SUBMITTED_KEY) - if (lastSubmittedStr) return isFeedbackTimeElapsed(lastSubmittedStr) - const firstRunStr = localStorage.getItem(APP_FIRST_STARTED_KEY) - if (!firstRunStr) return false - return isFeedbackTimeElapsed(firstRunStr) -} - -// Opt In/Out -const ifAnalyticsEnabled = func => () => { - if (analyticsEnabled && process.env.POSTHOG_TOKEN) { - return func() - } -} -const disabled = () => posthog.has_opted_out_capturing() -const optIn = () => posthog.opt_in_capturing() -const optOut = () => posthog.opt_out_capturing() - -export default { - activate, - identify, - identifyByApiKey, - captureException, - captureEvent, - requestFeedbackOnDeploy, - submitFeedback, - highlightFeedbackIcon, - disabled: () => { - if (analyticsEnabled == null) { - return true - } - return ifAnalyticsEnabled(disabled) - }, - optIn: ifAnalyticsEnabled(optIn), - optOut: ifAnalyticsEnabled(optOut), -} diff --git a/packages/builder/src/analytics/IntercomClient.js b/packages/builder/src/analytics/IntercomClient.js new file mode 100644 index 0000000000..8cc7e35bbf --- /dev/null +++ b/packages/builder/src/analytics/IntercomClient.js @@ -0,0 +1,94 @@ +export default class IntercomClient { + constructor(token) { + this.token = token + } + + /** + * Instantiate intercom using their provided script. + */ + init() { + if (!this.token) return + + const token = this.token + + var w = window + var ic = w.Intercom + if (typeof ic === "function") { + ic("reattach_activator") + ic("update", w.intercomSettings) + } else { + var d = document + var i = function () { + i.c(arguments) + } + i.q = [] + i.c = function (args) { + i.q.push(args) + } + w.Intercom = i + var l = function () { + var s = d.createElement("script") + s.type = "text/javascript" + s.async = true + s.src = "https://widget.intercom.io/widget/" + token + var x = d.getElementsByTagName("script")[0] + x.parentNode.insertBefore(s, x) + } + if (document.readyState === "complete") { + l() + } else if (w.attachEvent) { + w.attachEvent("onload", l) + } else { + w.addEventListener("load", l, false) + } + + this.initialised = true + } + } + + /** + * Show the intercom chat bubble. + * @param {Object} user - user to identify + * @returns Intercom global object + */ + show(user = {}) { + if (!this.initialised) return + + return window.Intercom("boot", { + app_id: this.token, + ...user, + }) + } + + /** + * Update intercom user details and messages. + * @returns Intercom global object + */ + update() { + if (!this.initialised) return + + return window.Intercom("update") + } + + /** + * Capture analytics events and send them to intercom. + * @param {String} event - event identifier + * @param {Object} props - properties for the event + * @returns Intercom global object + */ + captureEvent(event, props = {}) { + if (!this.initialised) return + + return window.Intercom("trackEvent", event, props) + } + + /** + * Disassociate the user from the current session. + * @returns Intercom global object + */ + logout() { + if (!this.initialised) return + + return window.Intercom("shutdown") + } +} diff --git a/packages/builder/src/analytics/PosthogClient.js b/packages/builder/src/analytics/PosthogClient.js new file mode 100644 index 0000000000..0a1fde42ea --- /dev/null +++ b/packages/builder/src/analytics/PosthogClient.js @@ -0,0 +1,80 @@ +import posthog from "posthog-js" +import { Events } from "./constants" + +export default class PosthogClient { + constructor(token, url) { + this.token = token + this.url = url + } + + init() { + if (!this.token || !this.url) return + + posthog.init(this.token, { + autocapture: false, + capture_pageview: false, + api_host: this.url, + }) + posthog.set_config({ persistence: "cookie" }) + + this.initialised = true + } + + /** + * Set the posthog context to the current user + * @param {String} id - unique user id + */ + identify(id) { + if (!this.initialised) return + + posthog.identify(id) + } + + /** + * Update user metadata associated with current user in posthog + * @param {Object} meta - user fields + */ + updateUser(meta) { + if (!this.initialised) return + + posthog.people.set(meta) + } + + /** + * Capture analytics events and send them to posthog. + * @param {String} event - event identifier + * @param {Object} props - properties for the event + */ + captureEvent(eventName, props) { + if (!this.initialised) return + + props.sourceApp = "builder" + posthog.capture(eventName, props) + } + + /** + * Submit NPS feedback to posthog. + * @param {Object} values - NPS Values + */ + npsFeedback(values) { + if (!this.initialised) return + + localStorage.setItem(Events.NPS.SUBMITTED, Date.now()) + + const prefixedFeedback = {} + for (let key in values) { + prefixedFeedback[`feedback_${key}`] = values[key] + } + + posthog.capture(Events.NPS.SUBMITTED, prefixedFeedback) + } + + /** + * Reset posthog user back to initial state on logout. + */ + logout() { + if (!this.initialised) return + + posthog.reset() + } +} diff --git a/packages/builder/src/analytics/SentryClient.js b/packages/builder/src/analytics/SentryClient.js new file mode 100644 index 0000000000..2a1f8732e3 --- /dev/null +++ b/packages/builder/src/analytics/SentryClient.js @@ -0,0 +1,37 @@ +import * as Sentry from "@sentry/browser" + +export default class SentryClient { + constructor(dsn) { + this.dsn = dsn + } + + init() { + if (this.dsn) { + Sentry.init({ dsn: this.dsn }) + + this.initalised = true + } + } + + /** + * Capture an exception and send it to sentry. + * @param {Error} err - JS error object + */ + captureException(err) { + if (!this.initalised) return + + Sentry.captureException(err) + } + + /** + * Identify user in sentry. + * @param {String} id - Unique user id + */ + identify(id) { + if (!this.initalised) return + + Sentry.configureScope(scope => { + scope.setUser({ id }) + }) + } +} diff --git a/packages/builder/src/analytics/constants.js b/packages/builder/src/analytics/constants.js new file mode 100644 index 0000000000..d38b7bba4f --- /dev/null +++ b/packages/builder/src/analytics/constants.js @@ -0,0 +1,49 @@ +export const Events = { + BUILDER: { + STARTED: "Builder Started", + }, + COMPONENT: { + CREATED: "Added Component", + }, + DATASOURCE: { + CREATED: "Datasource Created", + UPDATED: "Datasource Updated", + }, + TABLE: { + CREATED: "Table Created", + }, + VIEW: { + CREATED: "View Created", + ADDED_FILTER: "Added View Filter", + ADDED_CALCULATE: "Added View Calculate", + }, + SCREEN: { + CREATED: "Screen Created", + }, + AUTOMATION: { + CREATED: "Automation Created", + SAVED: "Automation Saved", + BLOCK_ADDED: "Added Automation Block", + }, + NPS: { + SUBMITTED: "budibase:feedback_submitted", + }, + APP: { + CREATED: "budibase:app_created", + PUBLISHED: "budibase:app_published", + UNPUBLISHED: "budibase:app_unpublished", + }, + ANALYTICS: { + OPT_IN: "budibase:analytics_opt_in", + OPT_OUT: "budibase:analytics_opt_out", + }, + USER: { + INVITE: "budibase:portal_user_invite", + }, + SMTP: { + SAVED: "budibase:smtp_saved", + }, + SSO: { + SAVED: "budibase:sso_saved", + }, +} diff --git a/packages/builder/src/analytics/index.js b/packages/builder/src/analytics/index.js new file mode 100644 index 0000000000..b79ab67e0c --- /dev/null +++ b/packages/builder/src/analytics/index.js @@ -0,0 +1,79 @@ +import api from "builderStore/api" +import PosthogClient from "./PosthogClient" +import IntercomClient from "./IntercomClient" +import SentryClient from "./SentryClient" +import { Events } from "./constants" +import { auth } from "stores/portal" +import { get } from "svelte/store" + +const posthog = new PosthogClient( + process.env.POSTHOG_TOKEN, + process.env.POSTHOG_URL +) +const sentry = new SentryClient(process.env.SENTRY_DSN) +const intercom = new IntercomClient(process.env.INTERCOM_TOKEN) + +class AnalyticsHub { + constructor() { + this.clients = [posthog, sentry, intercom] + } + + async activate() { + // Setting the analytics env var off in the backend overrides org/tenant settings + const analyticsStatus = await api.get("/api/analytics") + const json = await analyticsStatus.json() + + // Multitenancy disabled on the backend + if (!json.enabled) return + + const tenantId = get(auth).tenantId + + if (tenantId) { + const res = await api.get( + `/api/global/configs/public?tenantId=${tenantId}` + ) + const orgJson = await res.json() + + // analytics opted out for the tenant + if (orgJson.config?.analytics === false) return + } + + this.clients.forEach(client => client.init()) + this.enabled = true + } + + identify(id, metadata) { + posthog.identify(id) + if (metadata) { + posthog.updateUser(metadata) + } + sentry.identify(id) + } + + captureException(err) { + sentry.captureException(err) + } + + captureEvent(eventName, props = {}) { + posthog.captureEvent(eventName, props) + intercom.captureEvent(eventName, props) + } + + showChat(user) { + intercom.show(user) + } + + submitFeedback(values) { + posthog.npsFeedback(values) + } + + async logout() { + posthog.logout() + intercom.logout() + } +} + +const analytics = new AnalyticsHub() + +export { Events } +export default analytics diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index d3af6799f3..0858b29bcb 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -443,10 +443,9 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { for (let from of convertFromProps) { if (shouldReplaceBinding(newBoundValue, from, convertTo)) { const binding = bindableProperties.find(el => el[convertFrom] === from) - newBoundValue = newBoundValue.replace( - new RegExp(from, "gi"), - binding[convertTo] - ) + while (newBoundValue.includes(from)) { + newBoundValue = newBoundValue.replace(from, binding[convertTo]) + } } } result = result.replace(boundValue, newBoundValue) diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 6fecda84c0..f32dedd47e 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -3,7 +3,6 @@ import { getAutomationStore } from "./store/automation" import { getHostingStore } from "./store/hosting" import { getThemeStore } from "./store/theme" import { derived, writable } from "svelte/store" -import analytics from "analytics" import { FrontendTypes, LAYOUT_NAMES } from "../constants" import { findComponent } from "./storeUtils" @@ -55,13 +54,4 @@ export const mainLayout = derived(store, $store => { export const selectedAccessRole = writable("BASIC") -export const initialise = async () => { - try { - await analytics.activate() - analytics.captureEvent("Builder Started") - } catch (err) { - console.log(err) - } -} - export const screenSearchString = writable(null) diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index e60553070b..0a47970d28 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -2,7 +2,7 @@ import { writable } from "svelte/store" import api from "../../api" import Automation from "./Automation" import { cloneDeep } from "lodash/fp" -import analytics from "analytics" +import analytics, { Events } from "analytics" const automationActions = store => ({ fetch: async () => { @@ -110,7 +110,7 @@ const automationActions = store => ({ state.selectedBlock = newBlock return state }) - analytics.captureEvent("Added Automation Block", { + analytics.captureEvent(Events.AUTOMATION.BLOCK_ADDED, { name: block.name, }) }, diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 603fa88b09..09132f28cb 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -19,7 +19,7 @@ import { import { fetchComponentLibDefinitions } from "../loadComponentLibraries" import api from "../api" import { FrontendTypes } from "constants" -import analytics from "analytics" +import analytics, { Events } from "analytics" import { findComponentType, findComponentParent, @@ -215,6 +215,13 @@ export const getFrontendStore = () => { if (screenToDelete._id === 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 }) @@ -443,7 +450,7 @@ export const getFrontendStore = () => { }) // Log event - analytics.captureEvent("Added Component", { + analytics.captureEvent(Events.COMPONENT.CREATED, { name: componentInstance._component, }) @@ -646,6 +653,36 @@ export const getFrontendStore = () => { // Save layout await store.actions.layouts.save(layout) }, + delete: async (url, title) => { + const layout = get(mainLayout) + if (!layout) { + return + } + + // Add link setting to main layout + if (layout.props._component.endsWith("layout")) { + // If using a new SDK, add to the layout component settings + layout.props.links = layout.props.links.filter( + link => !(link.text === title && link.url === url) + ) + } else { + // If using an old SDK, add to the navigation component + // TODO: remove this when we can assume everyone has updated + const nav = findComponentType( + layout.props, + "@budibase/standard-components/navigation" + ) + if (!nav) { + return + } + + nav._children = nav._children.filter( + child => !(child.url === url && child.text === title) + ) + } + // Save layout + await store.actions.layouts.save(layout) + }, }, }, } diff --git a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte index 7700a4a1c2..f3273aa5ec 100644 --- a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte @@ -4,7 +4,7 @@ import { automationStore } from "builderStore" import { notifications } from "@budibase/bbui" import { Input, ModalContent, Layout, Body, Icon } from "@budibase/bbui" - import analytics from "analytics" + import analytics, { Events } from "analytics" let name let selectedTrigger @@ -36,7 +36,7 @@ notifications.success(`Automation ${name} created.`) $goto(`./${$automationStore.selectedAutomation.automation._id}`) - analytics.captureEvent("Automation Created", { name }) + analytics.captureEvent(Events.AUTOMATION.CREATED, { name }) } $: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER) diff --git a/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte index 29966ec372..64197c3a77 100644 --- a/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte @@ -2,7 +2,7 @@ import { automationStore } from "builderStore" import { notifications } from "@budibase/bbui" import { Icon, Input, ModalContent, Modal } from "@budibase/bbui" - import analytics from "analytics" + import analytics, { Events } from "analytics" let name let error = "" @@ -26,7 +26,7 @@ } await automationStore.actions.save(updatedAutomation) notifications.success(`Automation ${name} updated successfully.`) - analytics.captureEvent("Automation Saved", { name }) + analytics.captureEvent(Events.AUTOMATION.SAVED, { name }) hide() } diff --git a/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte b/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte index 660a822898..50d44eca88 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte @@ -1,7 +1,7 @@ diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte index 61777c0b7e..2f6ec51233 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte @@ -3,7 +3,7 @@ import { goto } from "@roxi/routify" import { views as viewsStore } from "stores/backend" import { tables } from "stores/backend" - import analytics from "analytics" + import analytics, { Events } from "analytics" let name let field @@ -21,7 +21,7 @@ field, }) notifications.success(`View ${name} created`) - analytics.captureEvent("View Created", { name }) + analytics.captureEvent(Events.VIEW.CREATED, { name }) $goto(`../../view/${name}`) } diff --git a/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte b/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte index 170bb75142..9c6f4956b0 100644 --- a/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte @@ -11,7 +11,7 @@ Icon, } from "@budibase/bbui" import { tables, views } from "stores/backend" - import analytics from "analytics" + import analytics, { Events } from "analytics" const CONDITIONS = [ { @@ -65,7 +65,7 @@ function saveView() { views.save(view) notifications.success(`View ${view.name} saved.`) - analytics.captureEvent("Added View Filter", { + analytics.captureEvent(Events.VIEW.ADDED_FILTER, { filters: JSON.stringify(view.filters), }) } diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte index 84c737eb67..6ba8e4042f 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte @@ -1,8 +1,9 @@ {#if $database?._id}
- {#each $datasources.list as datasource, idx} + {#each enrichedDataSources as datasource, idx} 0} text={datasource.name} - opened={openDataSources.includes(datasource._id)} - selected={$datasources.selected === datasource._id} + opened={datasource.open} + selected={datasource.selected} withArrow={true} on:click={() => selectDatasource(datasource)} on:iconClick={() => toggleNode(datasource)} @@ -61,22 +81,21 @@ {/if} - {#if openDataSources.includes(datasource._id)} + {#if datasource.open} + {#each $queries.list.filter(query => query.datasourceId === datasource._id) as query} + onClickQuery(query)} + > + + + {/each} {/if} - - {#each $queries.list.filter(query => query.datasourceId === datasource._id) as query} - onClickQuery(query)} - > - - - {/each} {/each}
{/if} diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte index 9cdd893230..e7affb30c4 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte @@ -5,7 +5,7 @@ import { Input, Label, ModalContent, Modal, Context } from "@budibase/bbui" import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte" import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte" - import analytics from "analytics" + import analytics, { Events } from "analytics" import { getContext } from "svelte" const modalContext = getContext(Context.Modal) @@ -45,7 +45,7 @@ plus, }) notifications.success(`Datasource ${name} created successfully.`) - analytics.captureEvent("Datasource Created", { name, type }) + analytics.captureEvent(Events.DATASOURCE.CREATED, { name, type }) // Navigate to new datasource $goto(`./datasource/${response._id}`) diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte index f93af59a38..28625aa86e 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte @@ -2,7 +2,7 @@ import { datasources } from "stores/backend" import { notifications } from "@budibase/bbui" import { Input, ModalContent, Modal } from "@budibase/bbui" - import analytics from "analytics" + import analytics, { Events } from "analytics" let error = "" let modal @@ -35,7 +35,7 @@ } await datasources.save(updatedDatasource) notifications.success(`Datasource ${name} updated successfully.`) - analytics.captureEvent("Datasource Updated", updatedDatasource) + analytics.captureEvent(Events.DATASOURCE.UPDATED, updatedDatasource) hide() } diff --git a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte index b59e5cda5e..dd8876be27 100644 --- a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte +++ b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte @@ -12,7 +12,7 @@ Layout, } from "@budibase/bbui" import TableDataImport from "../TableDataImport.svelte" - import analytics from "analytics" + import analytics, { Events } from "analytics" import screenTemplates from "builderStore/store/screenTemplates" import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils" import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen" @@ -67,7 +67,7 @@ // Create table const table = await tables.save(newTable) notifications.success(`Table ${name} created successfully.`) - analytics.captureEvent("Table Created", { name }) + analytics.captureEvent(Events.TABLE.CREATED, { name }) // Create auto screens if (createAutoscreens) { diff --git a/packages/builder/src/components/deploy/DeployModal.svelte b/packages/builder/src/components/deploy/DeployModal.svelte index 4daa16c7c4..3dcf0c27b1 100644 --- a/packages/builder/src/components/deploy/DeployModal.svelte +++ b/packages/builder/src/components/deploy/DeployModal.svelte @@ -2,7 +2,8 @@ import { onMount, onDestroy } from "svelte" import { Button, Modal, notifications, ModalContent } from "@budibase/bbui" import api from "builderStore/api" - import analytics from "analytics" + import analytics, { Events } from "analytics" + import { store } from "builderStore" const DeploymentStatus = { SUCCESS: "SUCCESS", @@ -23,6 +24,9 @@ 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) { diff --git a/packages/builder/src/components/design/NavigationPanel/NewScreenModal.svelte b/packages/builder/src/components/design/NavigationPanel/NewScreenModal.svelte index ed0c764956..e02f9d87e5 100644 --- a/packages/builder/src/components/design/NavigationPanel/NewScreenModal.svelte +++ b/packages/builder/src/components/design/NavigationPanel/NewScreenModal.svelte @@ -4,7 +4,7 @@ import { roles } from "stores/backend" import { Input, Select, ModalContent, Toggle } from "@budibase/bbui" import getTemplates from "builderStore/store/screenTemplates" - import analytics from "analytics" + import analytics, { Events } from "analytics" const CONTAINER = "@budibase/standard-components/container" @@ -66,7 +66,7 @@ if (templateIndex !== undefined) { const template = templates[templateIndex] - analytics.captureEvent("Screen Created", { + analytics.captureEvent(Events.SCREEN.CREATED, { template: template.id || template.name, }) } diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/RefreshDataProvider.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/RefreshDataProvider.svelte new file mode 100644 index 0000000000..fe251a0320 --- /dev/null +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/RefreshDataProvider.svelte @@ -0,0 +1,35 @@ + + +
+ +