forked from tsai/budibase
committed by
GitHub
136 changed files with 2053 additions and 998 deletions
|
After Width: | Height: | Size: 167 KiB |
@ -0,0 +1,50 @@ |
|||
<script> |
|||
import { notifications, ModalContent, Dropzone, Body } from "@budibase/bbui" |
|||
import { post } from "builderStore/api" |
|||
|
|||
let submitting = false |
|||
|
|||
$: value = { file: null } |
|||
|
|||
async function importApps() { |
|||
submitting = true |
|||
|
|||
try { |
|||
// Create form data to create app |
|||
let data = new FormData() |
|||
data.append("importFile", value.file) |
|||
|
|||
// Create App |
|||
const importResp = await post("/api/cloud/import", data, {}) |
|||
const importJson = await importResp.json() |
|||
if (!importResp.ok) { |
|||
throw new Error(importJson.message) |
|||
} |
|||
// now reload to get to login |
|||
window.location.reload() |
|||
} catch (error) { |
|||
notifications.error(error) |
|||
submitting = false |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Import apps" |
|||
confirmText="Import apps" |
|||
onConfirm={importApps} |
|||
disabled={!value.file} |
|||
> |
|||
<Body |
|||
>Please upload the file that was exported from your Cloud environment to get |
|||
started</Body |
|||
> |
|||
<Dropzone |
|||
gallery={false} |
|||
label="File to import" |
|||
value={[value.file]} |
|||
on:change={e => { |
|||
value.file = e.detail?.[0] |
|||
}} |
|||
/> |
|||
</ModalContent> |
|||
@ -0,0 +1,10 @@ |
|||
import API from "./api" |
|||
|
|||
/** |
|||
* Notifies that an end user client app has been loaded. |
|||
*/ |
|||
export const pingEndUser = async () => { |
|||
return await API.post({ |
|||
url: `/api/analytics/ping`, |
|||
}) |
|||
} |
|||
@ -0,0 +1,240 @@ |
|||
<script context="module"> |
|||
export const Sides = { |
|||
Top: "Top", |
|||
Right: "Right", |
|||
Bottom: "Bottom", |
|||
Left: "Left", |
|||
} |
|||
</script> |
|||
|
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { get } from "svelte/store" |
|||
import IndicatorSet from "./IndicatorSet.svelte" |
|||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte" |
|||
import { builderStore } from "stores" |
|||
|
|||
let dragInfo |
|||
let dropInfo |
|||
|
|||
const getEdges = (bounds, mousePoint) => { |
|||
const { width, height, top, left } = bounds |
|||
return { |
|||
[Sides.Top]: [mousePoint[0], top], |
|||
[Sides.Right]: [left + width, mousePoint[1]], |
|||
[Sides.Bottom]: [mousePoint[0], top + height], |
|||
[Sides.Left]: [left, mousePoint[1]], |
|||
} |
|||
} |
|||
|
|||
const calculatePointDelta = (point1, point2) => { |
|||
const deltaX = Math.abs(point1[0] - point2[0]) |
|||
const deltaY = Math.abs(point1[1] - point2[1]) |
|||
return Math.sqrt(deltaX * deltaX + deltaY * deltaY) |
|||
} |
|||
|
|||
const getDOMNodeForComponent = component => { |
|||
const parent = component.closest(".component") |
|||
const children = Array.from(parent.childNodes) |
|||
return children?.find(node => node?.nodeType === 1) |
|||
} |
|||
|
|||
// Callback when initially starting a drag on a draggable component |
|||
const onDragStart = e => { |
|||
const parent = e.target.closest(".component") |
|||
if (!parent?.classList.contains("draggable")) { |
|||
return |
|||
} |
|||
|
|||
// Update state |
|||
dragInfo = { |
|||
target: parent.dataset.id, |
|||
parent: parent.dataset.parent, |
|||
} |
|||
builderStore.actions.selectComponent(dragInfo.target) |
|||
builderStore.actions.setDragging(true) |
|||
|
|||
// Highlight being dragged by setting opacity |
|||
const child = getDOMNodeForComponent(e.target) |
|||
if (child) { |
|||
child.style.opacity = "0.5" |
|||
} |
|||
} |
|||
|
|||
// Callback when drag stops (whether dropped or not) |
|||
const onDragEnd = e => { |
|||
// Reset opacity style |
|||
if (dragInfo) { |
|||
const child = getDOMNodeForComponent(e.target) |
|||
if (child) { |
|||
child.style.opacity = "" |
|||
} |
|||
} |
|||
|
|||
// Reset state and styles |
|||
dragInfo = null |
|||
dropInfo = null |
|||
builderStore.actions.setDragging(false) |
|||
} |
|||
|
|||
// Callback when on top of a component |
|||
const onDragOver = e => { |
|||
// Skip if we aren't validly dragging currently |
|||
if (!dragInfo || !dropInfo) { |
|||
return |
|||
} |
|||
|
|||
e.preventDefault() |
|||
const { droppableInside, bounds } = dropInfo |
|||
const { top, left, height, width } = bounds |
|||
const mouseY = e.clientY |
|||
const mouseX = e.clientX |
|||
const snapFactor = droppableInside ? 0.33 : 0.5 |
|||
const snapLimitV = Math.min(40, height * snapFactor) |
|||
const snapLimitH = Math.min(40, width * snapFactor) |
|||
|
|||
// Determine all sies we are within snap range of |
|||
let sides = [] |
|||
if (mouseY <= top + snapLimitV) { |
|||
sides.push(Sides.Top) |
|||
} else if (mouseY >= top + height - snapLimitV) { |
|||
sides.push(Sides.Bottom) |
|||
} |
|||
if (mouseX < left + snapLimitH) { |
|||
sides.push(Sides.Left) |
|||
} else if (mouseX > left + width - snapLimitH) { |
|||
sides.push(Sides.Right) |
|||
} |
|||
|
|||
// When no edges match, drop inside if possible |
|||
if (!sides.length) { |
|||
dropInfo.mode = droppableInside ? "inside" : null |
|||
dropInfo.side = null |
|||
return |
|||
} |
|||
|
|||
// When one edge matches, use that edge |
|||
if (sides.length === 1) { |
|||
dropInfo.side = sides[0] |
|||
if ([Sides.Top, Sides.Left].includes(sides[0])) { |
|||
dropInfo.mode = "above" |
|||
} else { |
|||
dropInfo.mode = "below" |
|||
} |
|||
return |
|||
} |
|||
|
|||
// When 2 edges match, work out which is closer |
|||
const mousePoint = [mouseX, mouseY] |
|||
const edges = getEdges(bounds, mousePoint) |
|||
const edge1 = edges[sides[0]] |
|||
const delta1 = calculatePointDelta(mousePoint, edge1) |
|||
const edge2 = edges[sides[1]] |
|||
const delta2 = calculatePointDelta(mousePoint, edge2) |
|||
const edge = delta1 < delta2 ? sides[0] : sides[1] |
|||
dropInfo.side = edge |
|||
if ([Sides.Top, Sides.Left].includes(edge)) { |
|||
dropInfo.mode = "above" |
|||
} else { |
|||
dropInfo.mode = "below" |
|||
} |
|||
} |
|||
|
|||
// Callback when entering a potential drop target |
|||
const onDragEnter = e => { |
|||
// Skip if we aren't validly dragging currently |
|||
if (!dragInfo) { |
|||
return |
|||
} |
|||
|
|||
const element = e.target.closest(".component") |
|||
if ( |
|||
element && |
|||
element.classList.contains("droppable") && |
|||
element.dataset.id !== dragInfo.target |
|||
) { |
|||
// Do nothing if this is the same target |
|||
if (element.dataset.id === dropInfo?.target) { |
|||
return |
|||
} |
|||
|
|||
// Ensure the dragging flag is always set. |
|||
// There's a bit of a race condition between the app reinitialisation |
|||
// after selecting the DND component and setting this the first time |
|||
if (!get(builderStore).isDragging) { |
|||
builderStore.actions.setDragging(true) |
|||
} |
|||
|
|||
// Store target ID |
|||
const target = element.dataset.id |
|||
|
|||
// Precompute and store some info to avoid recalculating everything in |
|||
// dragOver |
|||
const child = getDOMNodeForComponent(e.target) |
|||
const bounds = child.getBoundingClientRect() |
|||
dropInfo = { |
|||
target, |
|||
name: element.dataset.name, |
|||
droppableInside: element.classList.contains("empty"), |
|||
bounds, |
|||
} |
|||
} else { |
|||
dropInfo = null |
|||
} |
|||
} |
|||
|
|||
// Callback when leaving a potential drop target. |
|||
// Since we don't style our targets, we don't need to unset anything. |
|||
const onDragLeave = () => {} |
|||
|
|||
// Callback when dropping a drag on top of some component |
|||
const onDrop = e => { |
|||
e.preventDefault() |
|||
if (dropInfo?.mode) { |
|||
builderStore.actions.moveComponent( |
|||
dragInfo.target, |
|||
dropInfo.target, |
|||
dropInfo.mode |
|||
) |
|||
} |
|||
} |
|||
|
|||
onMount(() => { |
|||
// Events fired on the draggable target |
|||
document.addEventListener("dragstart", onDragStart, false) |
|||
document.addEventListener("dragend", onDragEnd, false) |
|||
|
|||
// Events fired on the drop targets |
|||
document.addEventListener("dragover", onDragOver, false) |
|||
document.addEventListener("dragenter", onDragEnter, false) |
|||
document.addEventListener("dragleave", onDragLeave, false) |
|||
document.addEventListener("drop", onDrop, false) |
|||
|
|||
return () => { |
|||
// Events fired on the draggable target |
|||
document.removeEventListener("dragstart", onDragStart, false) |
|||
document.removeEventListener("dragend", onDragEnd, false) |
|||
|
|||
// Events fired on the drop targets |
|||
document.removeEventListener("dragover", onDragOver, false) |
|||
document.removeEventListener("dragenter", onDragEnter, false) |
|||
document.removeEventListener("dragleave", onDragLeave, false) |
|||
document.removeEventListener("drop", onDrop, false) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<IndicatorSet |
|||
componentId={dropInfo?.mode === "inside" ? dropInfo.target : null} |
|||
color="var(--spectrum-global-color-static-green-500)" |
|||
zIndex="930" |
|||
transition |
|||
prefix="Inside" |
|||
/> |
|||
|
|||
<DNDPositionIndicator |
|||
{dropInfo} |
|||
color="var(--spectrum-global-color-static-green-500)" |
|||
zIndex="940" |
|||
transition |
|||
/> |
|||
@ -0,0 +1,64 @@ |
|||
<script> |
|||
import Indicator from "./Indicator.svelte" |
|||
import { Sides } from "./DNDHandler.svelte" |
|||
|
|||
export let dropInfo |
|||
export let zIndex |
|||
export let color |
|||
export let transition |
|||
|
|||
$: dimensions = getDimensions(dropInfo) |
|||
$: prefix = dropInfo?.mode === "above" ? "Before" : "After" |
|||
$: text = `${prefix} ${dropInfo?.name}` |
|||
$: renderKey = `${dropInfo?.target}-${dropInfo?.side}` |
|||
|
|||
const getDimensions = info => { |
|||
const { bounds, side } = info ?? {} |
|||
if (!bounds || !side) { |
|||
return null |
|||
} |
|||
|
|||
// Get preview offset |
|||
const root = document.getElementById("clip-root") |
|||
const rootBounds = root.getBoundingClientRect() |
|||
|
|||
// Subtract preview offset from bounds |
|||
let { left, top, width, height } = bounds |
|||
left -= rootBounds.left |
|||
top -= rootBounds.top |
|||
|
|||
// Determine position |
|||
if (side === Sides.Top || side === Sides.Bottom) { |
|||
return { |
|||
top: side === Sides.Top ? top - 4 : top + height, |
|||
left: left - 2, |
|||
width: width + 4, |
|||
height: 0, |
|||
} |
|||
} else { |
|||
return { |
|||
top: top - 2, |
|||
left: side === Sides.Left ? left - 4 : left + width, |
|||
width: 0, |
|||
height: height + 4, |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
{#key renderKey} |
|||
{#if dimensions && dropInfo?.mode !== "inside"} |
|||
<Indicator |
|||
left={Math.round(dimensions.left)} |
|||
top={Math.round(dimensions.top)} |
|||
width={dimensions.width} |
|||
height={dimensions.height} |
|||
{text} |
|||
{zIndex} |
|||
{color} |
|||
{transition} |
|||
alignRight={dropInfo?.side === Sides.Right} |
|||
line |
|||
/> |
|||
{/if} |
|||
{/key} |
|||
@ -0,0 +1,28 @@ |
|||
version: "3.8" |
|||
services: |
|||
db: |
|||
container_name: postgres |
|||
image: postgres |
|||
restart: always |
|||
environment: |
|||
POSTGRES_USER: root |
|||
POSTGRES_PASSWORD: root |
|||
POSTGRES_DB: main |
|||
ports: |
|||
- "5432:5432" |
|||
volumes: |
|||
#- pg_data:/var/lib/postgresql/data/ |
|||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql |
|||
|
|||
pgadmin: |
|||
container_name: pgadmin-pg |
|||
image: dpage/pgadmin4 |
|||
restart: always |
|||
environment: |
|||
PGADMIN_DEFAULT_EMAIL: root@root.com |
|||
PGADMIN_DEFAULT_PASSWORD: root |
|||
ports: |
|||
- "5050:80" |
|||
|
|||
#volumes: |
|||
# pg_data: |
|||
@ -0,0 +1,41 @@ |
|||
SELECT 'CREATE DATABASE main' |
|||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec |
|||
|
|||
CREATE TABLE categories |
|||
( |
|||
name text COLLATE pg_catalog."default", |
|||
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ), |
|||
CONSTRAINT categories_pkey PRIMARY KEY (id) |
|||
); |
|||
|
|||
CREATE TABLE customers |
|||
( |
|||
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ), |
|||
name text COLLATE pg_catalog."default", |
|||
email text COLLATE pg_catalog."default", |
|||
age integer, |
|||
"dateOfBirth" date, |
|||
CONSTRAINT customers_pkey PRIMARY KEY (id) |
|||
); |
|||
|
|||
CREATE TABLE customer_category |
|||
( |
|||
customer_id integer, |
|||
category_id integer, |
|||
notes text COLLATE pg_catalog."default", |
|||
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ), |
|||
CONSTRAINT "Category" FOREIGN KEY (category_id) |
|||
REFERENCES public.categories (id) MATCH SIMPLE |
|||
ON UPDATE NO ACTION |
|||
ON DELETE NO ACTION |
|||
NOT VALID, |
|||
CONSTRAINT "Customer" FOREIGN KEY (customer_id) |
|||
REFERENCES public.customers (id) MATCH SIMPLE |
|||
ON UPDATE NO ACTION |
|||
ON DELETE NO ACTION |
|||
NOT VALID |
|||
); |
|||
|
|||
|
|||
INSERT INTO customers (name, email, age) VALUES ('Mike', 'mike@mike.com', 30); |
|||
INSERT INTO categories (name) VALUES ('Books'); |
|||
@ -0,0 +1,3 @@ |
|||
#!/bin/bash |
|||
docker-compose down |
|||
docker volume prune -f |
|||
@ -0,0 +1,22 @@ |
|||
#!/usr/bin/env node
|
|||
const updateDotEnv = require("update-dotenv") |
|||
|
|||
const arg = process.argv.slice(2)[0] |
|||
|
|||
/** |
|||
* For testing multi tenancy sub domains locally. |
|||
* |
|||
* Relies on an entry in /etc/hosts e.g: |
|||
* |
|||
* 127.0.0.1 local.com |
|||
* |
|||
* and an entry for each tenant you wish to test locally e.g: |
|||
* |
|||
* 127.0.0.1 t1.local.com |
|||
* 127.0.0.1 t2.local.com |
|||
*/ |
|||
updateDotEnv({ |
|||
ACCOUNT_PORTAL_URL: |
|||
arg === "enable" ? "http://local.com:10001" : "http://localhost:10001", |
|||
COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "", |
|||
}).then(() => console.log("Updated worker!")) |
|||
@ -1,7 +1,32 @@ |
|||
const env = require("../../environment") |
|||
const PostHog = require("posthog-node") |
|||
|
|||
exports.isEnabled = async function (ctx) { |
|||
let posthogClient |
|||
|
|||
if (env.POSTHOG_TOKEN && env.ENABLE_ANALYTICS && !env.SELF_HOSTED) { |
|||
posthogClient = new PostHog(env.POSTHOG_TOKEN) |
|||
} |
|||
|
|||
exports.isEnabled = async ctx => { |
|||
ctx.body = { |
|||
enabled: !env.SELF_HOSTED && env.ENABLE_ANALYTICS === "true", |
|||
} |
|||
} |
|||
|
|||
exports.endUserPing = async ctx => { |
|||
if (!posthogClient) { |
|||
ctx.body = { |
|||
ping: false, |
|||
} |
|||
return |
|||
} |
|||
|
|||
posthogClient.capture("budibase:end_user_ping", { |
|||
userId: ctx.user && ctx.user._id, |
|||
appId: ctx.appId, |
|||
}) |
|||
|
|||
ctx.body = { |
|||
ping: true, |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,92 @@ |
|||
const env = require("../../environment") |
|||
const { getAllApps } = require("@budibase/auth/db") |
|||
const CouchDB = require("../../db") |
|||
const { |
|||
exportDB, |
|||
sendTempFile, |
|||
readFileSync, |
|||
} = require("../../utilities/fileSystem") |
|||
const { stringToReadStream } = require("../../utilities") |
|||
const { getGlobalDBName, getGlobalDB } = require("@budibase/auth/tenancy") |
|||
const { create } = require("./application") |
|||
const { getDocParams, DocumentTypes, isDevAppID } = require("../../db/utils") |
|||
|
|||
async function createApp(appName, appImport) { |
|||
const ctx = { |
|||
request: { |
|||
body: { |
|||
templateString: appImport, |
|||
name: appName, |
|||
}, |
|||
}, |
|||
} |
|||
return create(ctx) |
|||
} |
|||
|
|||
exports.exportApps = async ctx => { |
|||
if (env.SELF_HOSTED || !env.MULTI_TENANCY) { |
|||
ctx.throw(400, "Exporting only allowed in multi-tenant cloud environments.") |
|||
} |
|||
const apps = await getAllApps(CouchDB, { all: true }) |
|||
const globalDBString = await exportDB(getGlobalDBName()) |
|||
let allDBs = { |
|||
global: globalDBString, |
|||
} |
|||
for (let app of apps) { |
|||
// only export the dev apps as they will be the latest, the user can republish the apps
|
|||
// in their self hosted environment
|
|||
if (isDevAppID(app._id)) { |
|||
allDBs[app.name] = await exportDB(app._id) |
|||
} |
|||
} |
|||
const filename = `cloud-export-${new Date().getTime()}.txt` |
|||
ctx.attachment(filename) |
|||
ctx.body = sendTempFile(JSON.stringify(allDBs)) |
|||
} |
|||
|
|||
async function getAllDocType(db, docType) { |
|||
const response = await db.allDocs( |
|||
getDocParams(docType, null, { |
|||
include_docs: true, |
|||
}) |
|||
) |
|||
return response.rows.map(row => row.doc) |
|||
} |
|||
|
|||
exports.importApps = async ctx => { |
|||
if (!env.SELF_HOSTED || env.MULTI_TENANCY) { |
|||
ctx.throw(400, "Importing only allowed in self hosted environments.") |
|||
} |
|||
const apps = await getAllApps(CouchDB, { all: true }) |
|||
if ( |
|||
apps.length !== 0 || |
|||
!ctx.request.files || |
|||
!ctx.request.files.importFile |
|||
) { |
|||
ctx.throw( |
|||
400, |
|||
"Import file is required and environment must be fresh to import apps." |
|||
) |
|||
} |
|||
const importFile = ctx.request.files.importFile |
|||
const importString = readFileSync(importFile.path) |
|||
const dbs = JSON.parse(importString) |
|||
const globalDbImport = dbs.global |
|||
// remove from the list of apps
|
|||
delete dbs.global |
|||
const globalDb = getGlobalDB() |
|||
// load the global db first
|
|||
await globalDb.load(stringToReadStream(globalDbImport)) |
|||
for (let [appName, appImport] of Object.entries(dbs)) { |
|||
await createApp(appName, appImport) |
|||
} |
|||
// once apps are created clean up the global db
|
|||
let users = await getAllDocType(globalDb, DocumentTypes.USER) |
|||
for (let user of users) { |
|||
delete user.tenantId |
|||
} |
|||
await globalDb.bulkDocs(users) |
|||
ctx.body = { |
|||
message: "Apps successfully imported.", |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue