Merge branch 'develop' of github.com:Budibase/budibase into cheeks-lab-day-devtools

This commit is contained in:
Andrew Kingston 2022-04-06 11:50:43 +01:00
commit c2d61d8ebc
406 changed files with 29102 additions and 3457 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
packages/server/node_modules
packages/builder
packages/frontend-core
packages/backend-core
packages/worker/node_modules
packages/cli
packages/client
packages/bbui
packages/string-templates

View File

@ -24,9 +24,28 @@
{
"files": ["*.svelte"],
"processor": "svelte3/svelte3"
},
{
"files": ["**/*.ts"],
"parser": "@typescript-eslint/parser",
"plugins": [],
"extends": [
"eslint:recommended"
],
"rules": {
"no-unused-vars": "off",
"no-inner-declarations": "off",
"no-case-declarations": "off",
"no-useless-escape": "off",
"no-undef": "off",
"no-prototype-builtins": "off"
}
}
],
"rules": {
"no-self-assign": "off"
},
"globals": {
"GeolocationPositionError": true
}
}

View File

@ -38,6 +38,17 @@ jobs:
fi
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Proxy service docker image
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build:docker:proxy:prod
docker tag proxy-service budibase/proxy:$PROD_TAG
docker push budibase/proxy:$PROD_TAG
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
PROD_TAG: k8s
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:

View File

@ -23,12 +23,24 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Get the latest budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Proxy service docker image
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build:docker:proxy:preprod
docker tag proxy-service budibase/proxy:$PREPROD_TAG
docker push budibase/proxy:$PREPROD_TAG
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
PREPROD_TAG: k8s-preprod
- name: Pull values.yaml from budibase-infra
run: |
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \

View File

@ -48,6 +48,13 @@ jobs:
yarn build
popd
- name: Build OpenAPI spec
run: |
pushd packages/server
yarn
yarn specs
popd
- name: Setup Helm
uses: azure/setup-helm@v1
id: helm-install
@ -56,6 +63,7 @@ jobs:
run: |
git config user.name "Budibase Helm Bot"
git config user.email "<>"
git reset --hard
git pull
helm package charts/budibase
git checkout gh-pages
@ -77,3 +85,5 @@ jobs:
packages/cli/build/cli-win.exe
packages/cli/build/cli-linux
packages/cli/build/cli-macos
packages/server/specs/openapi.yaml
packages/server/specs/openapi.json

View File

@ -2,6 +2,8 @@ name: Budibase Smoke Test
on:
workflow_dispatch:
schedule:
- cron: "0 5 * * *" # every day at 5AM
jobs:
release:
@ -23,10 +25,13 @@ jobs:
-o packages/builder/cypress.env.json \
-L https://api.github.com/repos/budibase/budibase-infra/contents/test/cypress.env.json
wc -l packages/builder/cypress.env.json
- run: yarn test:e2e:ci
env:
CI: true
name: Budibase CI
- name: Cypress run
id: cypress
uses: cypress-io/github-action@v2
with:
install: false
command: yarn test:e2e:ci
# TODO: upload recordings to s3
# - name: Configure AWS Credentials
@ -36,11 +41,11 @@ jobs:
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# aws-region: eu-west-1
# TODO look at cypress reporters
# - name: Discord Webhook Action
# uses: tsickert/discord-webhook@v4.0.0
# with:
# webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
# content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
# embed-title: ${{ env.RELEASE_VERSION }}
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with:
webhook-url: ${{ secrets.BUDI_QA_WEBHOOK }}
content: "Smoke test run completed with ${{ steps.cypress.outcome }}. See results at ${{ steps.cypress.dashboardUrl }}"
embed-title: ${{ steps.cypress.outcome }}
embed-color: ${{ steps.cypress.outcome == 'success' && '3066993' || '15548997' }}

2
.gitignore vendored
View File

@ -96,4 +96,6 @@ hosting/proxy/.generated-nginx.prod.conf
*.sublime-workspace
bin/
hosting/.generated*
packages/builder/cypress.env.json
stats.html

View File

@ -1,11 +1,11 @@
node_modules
public
dist
*.spec.js
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
packages/server/builder
packages/server/coverage
packages/server/client
packages/server/src/definitions/openapi.ts
packages/builder/.routify
packages/builder/cypress/support/queryLevelTransformerFunction.js
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js

11
.vscode/launch.json vendored
View File

@ -22,9 +22,16 @@
"name": "Budibase Worker",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/packages/worker/src/index.js",
"runtimeArgs": [
"--nolazy",
"-r",
"ts-node/register/transpile-only"
],
"args": [
"${workspaceFolder}/packages/worker/src/index.ts"
],
"cwd": "${workspaceFolder}/packages/worker"
}
},
],
"compounds": [
{

View File

@ -11,7 +11,7 @@
The low code platform you'll enjoy using
</h3>
<p align="center">
Budibase is an open source low-code platform, and the easiest way to build internal tools that improve productivity.
Budibase is an open source low-code platform, and the easiest way to build internal apps that improve productivity.
</p>
<h3 align="center">
@ -40,9 +40,11 @@
</p>
<h3 align="center">
<a href="https://docs.budibase.com/getting-started">Get started</a>
<a href="https://account.budibase.app/register">Get started - we host (Budibase Cloud)</a>
<span> · </span>
<a href="https://docs.budibase.com">Docs</a>
<a href="https://docs.budibase.com/docs/hosting-methods">Get started - you host (Docker, K8s, DO)</a>
<span> · </span>
<a href="https://docs.budibase.com/docs">Docs</a>
<span> · </span>
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
<span> · </span>
@ -100,16 +102,45 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
<br />
---
<br />
## Budibase Public API
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
- Budibase as a backend
- Interoperability
#### Docs
You can learn more about the Budibase API at the following places:
- [General documentation](https://docs.budibase.com/docs/public-api) : Learn how to get your API key, how to use spec, and how to use with Postman
- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
#### Guides
- [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
<p align="center">
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1647858558/Feb%20release/Start_building_with_Budibase_s_API_3_rhlzhv.png">
</p>
<br /><br />
<br /><br /><br />
## 🏁 Get started
<a href="https://docs.budibase.com/self-hosting/self-host"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
<a href="https://docs.budibase.com/docs/hosting-methods"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
### [Get started with self-hosting Budibase](https://docs.budibase.com/self-hosting/self-host)
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
### [Get started with Budibase Cloud](https://budibase.com)
@ -118,7 +149,7 @@ Or use Budibase Cloud if you don't need to self-host, and would like to get star
## 🎓 Learning Budibase
The Budibase documentation [lives here](https://docs.budibase.com).
The Budibase documentation [lives here](https://docs.budibase.com/docs).
<br />

View File

@ -15,7 +15,7 @@ version: 0.2.8
appVersion: 1.0.48
dependencies:
- name: couchdb
version: 3.3.4
version: 3.6.1
repository: https://apache.github.io/couchdb-helm
condition: services.couchdb.enabled
- name: ingress-nginx

View File

@ -110,6 +110,10 @@ spec:
value: {{ .Values.globals.cookieDomain | quote }}
- name: HTTP_MIGRATIONS
value: {{ .Values.globals.httpMigrations | quote }}
- name: GOOGLE_CLIENT_ID
value: {{ .Values.globals.google.clientId | quote }}
- name: GOOGLE_CLIENT_SECRET
value: {{ .Values.globals.google.secret | quote }}
image: budibase/apps:{{ .Values.globals.appVersion }}
imagePullPolicy: Always
name: bbapps

View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

38
examples/nextjs-api-sales/.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# typescript
*.tsbuildinfo

View File

@ -0,0 +1,41 @@
# Budibase API + Next.js example
This is an example of how Budibase can be used as a backend for a Postgres database for a Next.js sales app. You will
need to follow the walk-through that has been published in the Budibase docs to set up your Budibase app for this example.
## Pre-requisites
To use this example you will need:
1. [Docker](https://www.docker.com/)
2. [Docker Compose](https://docs.docker.com/compose/)
3. [Node.js](https://nodejs.org/en/)
4. A self-hosted Budibase installation
## Getting Started
The first step is to set up the database - you can do this by going to the `db/` directory and running the command:
```bash
docker-compose up
```
The next step is to follow the example walk-through and set up a Budibase app as it describes. Once you've done
this you can configure the settings in `next.config.js`, specifically the `apiKey`, `host` and `appName`.
Finally, you can start the dev server with the following command:
```bash
npm run dev
# or
yarn dev
```
## Accessing the app
Open [http://localhost:3001](http://localhost:3001) with your browser to see the sales app.
Look in the API routes (`pages/api/sales.ts` and `pages/api/salespeople.ts`) to see how this is integrated with Budibase.
There is also a utility file where some core functions and types have been defined, in `utilities/index.ts`.
## Attribution
This example was set up using [Next.js](https://nextjs.org/) and bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

View File

@ -0,0 +1,42 @@
import Link from "next/link"
import Image from "next/image"
import { ReactNotifications } from "react-notifications-component"
function layout(props: any) {
return (
<>
<nav className="navbar" role="navigation" aria-label="main navigation">
<div id="navbar" className="navbar-menu">
<div className="logo">
<Image alt="logo" src="/bb-emblem.svg" width="50" height="50" />
</div>
<div className="navbar-start">
<Link href="/">
<a className="navbar-item">
List
</a>
</Link>
<Link href="/save">
<a className="navbar-item">
New sale
</a>
</Link>
</div>
<div className="navbar-end">
<div className="navbar-item">
<div className="buttons">
<a className="button is-primary" href="https://budibase.readme.io/reference">
<strong>API Documentation</strong>
</a>
</div>
</div>
</div>
</div>
</nav>
<ReactNotifications />
{props.children}
</>
)
}
export default layout

View File

@ -0,0 +1,28 @@
import { Store } from "react-notifications-component"
const notifications = {
error: (error: string, title: string) => {
Store.addNotification({
container: "top-right",
type: "danger",
message: error,
title: title,
dismiss: {
duration: 10000,
},
})
},
success: (message: string, title: string) => {
Store.addNotification({
container: "top-right",
type: "success",
message: message,
title: title,
dismiss: {
duration: 3000,
},
})
},
}
export default notifications

View File

@ -0,0 +1,17 @@
version: "3.8"
services:
db:
container_name: postgres
image: postgres
restart: always
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: root
POSTGRES_DB: postgres
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data/
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
volumes:
pg_data:

View File

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS sales_people (
person_id SERIAL PRIMARY KEY,
name varchar(200) NOT NULL
);
CREATE TABLE IF NOT EXISTS sales (
sale_id SERIAL PRIMARY KEY,
sale_name varchar(200) NOT NULL,
sold_by INT,
CONSTRAINT sold_by_fk
FOREIGN KEY(sold_by)
REFERENCES sales_people(person_id)
);
INSERT INTO sales_people (name)
select 'Salesperson ' || id
FROM GENERATE_SERIES(1, 50) as id;
INSERT INTO sales (sale_name, sold_by)
select 'Sale ' || id, floor(random() * 50 + 1)::int
FROM GENERATE_SERIES(1, 200) as id;

View File

@ -0,0 +1,7 @@
import { components } from "./openapi"
export type App = components["schemas"]["applicationOutput"]["data"]
export type Table = components["schemas"]["tableOutput"]["data"]
export type TableSearch = components["schemas"]["tableSearch"]
export type AppSearch = components["schemas"]["applicationSearch"]
export type RowSearch = components["schemas"]["searchOutput"]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -0,0 +1,16 @@
const { join } = require("path")
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
sassOptions: {
includePaths: [join(__dirname, "styles")],
},
serverRuntimeConfig: {
apiKey:
"bf4d86af933b5ac0af0fdbe4bf7d89ff-f929752a1eeaafb00f4b5e3325097d51a44fe4b39f22ed857923409cc75414b379323a25ebfb4916",
appName: "sales",
host: "http://localhost:10000",
},
}
module.exports = nextConfig

View File

@ -0,0 +1,27 @@
{
"name": "nextjs-api-sales",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"bulma": "^0.9.3",
"next": "12.1.0",
"node-fetch": "^3.2.2",
"node-sass": "^7.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-notifications-component": "^3.4.1"
},
"devDependencies": {
"@types/node": "17.0.21",
"@types/react": "17.0.39",
"eslint": "8.10.0",
"eslint-config-next": "12.1.0",
"typescript": "4.6.2"
}
}

View File

@ -0,0 +1,17 @@
import "../styles/global.sass"
import type { AppProps } from "next/app"
import Head from "next/head"
import Layout from "../components/layout"
function MyApp({ Component, pageProps }: AppProps) {
return (
<Layout>
<Head>
<title>BB NextJS Sales Example</title>
</Head>
<Component {...pageProps} />
</Layout>
)
}
export default MyApp

View File

@ -0,0 +1,46 @@
import { getApp, findTable, makeCall } from "../../utilities"
async function getSales(req: any) {
const { page } = req.query
const { _id: appId } = await getApp()
const table = await findTable(appId, "sales")
return await makeCall("post", `tables/${table._id}/rows/search`, {
appId,
body: {
limit: 10,
sort: {
type: "string",
order: "descending",
column: "sale_id",
},
paginate: true,
bookmark: parseInt(page),
},
})
}
async function saveSale(req: any) {
const { _id: appId } = await getApp()
const table = await findTable(appId, "sales")
return await makeCall("post", `tables/${table._id}/rows`, {
body: req.body,
appId,
})
}
export default async function handler(req: any, res: any) {
let response: any = {}
try {
if (req.method === "POST") {
response = await saveSale(req)
} else if (req.method === "GET") {
response = await getSales(req)
} else {
res.status(404)
return
}
res.status(200).json(response)
} catch (err: any) {
res.status(400).send(err)
}
}

View File

@ -0,0 +1,31 @@
import { getApp, findTable, makeCall } from "../../utilities"
async function getSalespeople() {
const { _id: appId } = await getApp()
const table = await findTable(appId, "sales_people")
return await makeCall("post", `tables/${table._id}/rows/search`, {
appId,
body: {
sort: {
type: "string",
order: "ascending",
column: "person_id",
},
},
})
}
export default async function handler(req: any, res: any) {
let response: any = {}
try {
if (req.method === "GET") {
response = await getSalespeople()
} else {
res.status(404)
return
}
res.status(200).json(response)
} catch (err: any) {
res.status(400).send(err)
}
}

View File

@ -0,0 +1,83 @@
import type { NextPage } from "next"
import styles from "../styles/home.module.css"
import { useState, useEffect, useCallback } from "react"
import Notifications from "../components/notifications"
const Home: NextPage = () => {
const [sales, setSales] = useState([])
const [currentPage, setCurrentPage] = useState(1)
const [loaded, setLoaded] = useState(false)
const getSales = useCallback(async (page: Number = 1) => {
let url = "/api/sales"
if (page) {
url += `?page=${page}`
}
const response = await fetch(url)
if (!response.ok) {
const error = await response.text()
Notifications.error(error, "Failed to get sales")
return
}
const sales = await response.json()
// @ts-ignore
setCurrentPage(page)
return setSales(sales.data)
}, [])
const goToNextPage = useCallback(async () => {
await getSales(currentPage + 1)
}, [currentPage, getSales])
const goToPrevPage = useCallback(async () => {
if (currentPage > 1) {
await getSales(currentPage - 1)
}
}, [currentPage, getSales])
useEffect(() => {
getSales().then(() => {
setLoaded(true)
}).catch(() => {
setSales([])
})
}, [])
if (!loaded) {
return null
}
return (
<div className={styles.container}>
<div className={styles.tableSection}>
<h1 className="subtitle">Sales</h1>
<div className={styles.table}>
<table className="table">
<thead>
<tr>
<th>Sale ID</th>
<th>name</th>
<th>Sold by</th>
</tr>
</thead>
<tbody>
{sales.map((sale: any) =>
<tr key={sale.sale_id}>
<th>{sale.sale_id}</th>
<th>{sale.sale_name}</th>
<th>{sale.sales_person?.map((person: any) => person.primaryDisplay)[0]}</th>
</tr>
)}
</tbody>
</table>
<div className={styles.buttons}>
<button className="button" onClick={goToPrevPage}>Prev Page</button>
<button className="button" onClick={goToNextPage}>Next Page</button>
</div>
</div>
</div>
</div>
)
}
export default Home

View File

@ -0,0 +1,81 @@
import type { NextPage } from "next"
import { useCallback, useEffect, useState } from "react"
import styles from "../styles/save.module.css"
import Notifications from "../components/notifications"
const Save: NextPage = () => {
const [salespeople, setSalespeople] = useState([])
const [loaded, setLoaded] = useState(false)
const saveSale = useCallback(async (event: any) => {
event.preventDefault()
const sale = {
sale_name: event.target.name.value,
sales_person: [event.target.soldBy.value],
}
const response = await fetch("/api/sales", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(sale),
})
if (!response.ok) {
const error = await response.text()
Notifications.error(error, "Failed to save sale")
return
}
Notifications.success("Sale saved successfully!", "Sale saved")
}, [])
const getSalespeople = useCallback(async () => {
const response: any = await fetch("/api/salespeople")
if (!response.ok) {
throw new Error(await response.text())
}
const json = await response.json()
setSalespeople(json.data)
}, [])
useEffect(() => {
getSalespeople().then(() => {
setLoaded(true)
}).catch(() => {
setSalespeople([])
})
}, [])
if (!loaded) {
return null
}
return (
<div className={styles.container}>
<div className={styles.formSection}>
<h1 className="subtitle">New sale</h1>
<form onSubmit={saveSale}>
<div className="field">
<label className="label">Name</label>
<div className="control">
<input id="name" className="input" type="text" placeholder="Text input" />
</div>
</div>
<div className="field">
<label className="label">Sold by</label>
<div className="control">
<div className="select">
<select id="soldBy">
{salespeople.map((person: any) => <option key={person._id} value={person._id}>{person.name}</option>)}
</select>
</div>
</div>
</div>
<div className="control">
<button className="button is-link">Submit</button>
</div>
</form>
</div>
</div>
)
}
export default Save

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#393C44;}
.st1{fill:#FFFFFF;}
.st2{fill:#4285F4;}
</style>
<rect x="-152.17" y="-24.17" class="st0" width="96.17" height="96.17"/>
<path class="st1" d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"/>
<g>
<g>
<path class="st0" d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-93.55,28.92-93.46,28.52-93.46,28.11z"/>
</g>
<g>
<path class="st0" d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C-108.68,28.92-108.6,28.52-108.6,28.11z"/>
</g>
</g>
<path class="st2" d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"/>
<g>
<g>
<path class="st1" d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
C34.45,139.92,34.54,139.52,34.54,139.11z"/>
</g>
<g>
<path class="st1" d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"/>
</g>
</g>
<g>
<path class="st0" d="M44,48H4c-2.21,0-4-1.79-4-4V4c0-2.21,1.79-4,4-4h40c2.21,0,4,1.79,4,4v40C48,46.21,46.21,48,44,48z"/>
<g>
<path class="st1" d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
/>
</g>
<g>
<path class="st1" d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,26 @@
@charset "utf-8"
@import url('https://fonts.googleapis.com/css?family=Roboto:400,700')
$family-sans-serif: "Roboto", sans-serif
#__next
display: flex
flex-direction: column
justify-content: flex-start
align-items: stretch
height: 100vh
--bg-color: #f5f5f5
.logo
padding: 0.75rem
@import "../node_modules/bulma/bulma.sass"
@import "../node_modules/react-notifications-component/dist/theme.css"
// applied after bulma styles are enabled
html
overflow-y: auto
.navbar
background-color: var(--bg-color)
color: white

View File

@ -0,0 +1,30 @@
.container {
width: 100vw;
display: flex;
flex-direction: column;
padding: 5rem 2rem 0;
align-items: center;
flex: 1 1 auto;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.tableSection {
padding: 2rem;
background: var(--bg-color);
width: 800px;
border-radius: 10px;
}
.table table {
width: 100%;
}
.tableSection h1 {
text-align: center;
color: black;
}

View File

@ -0,0 +1,20 @@
.container {
width: 100vw;
display: flex;
flex-direction: column;
padding: 5rem 2rem 0;
align-items: center;
flex: 1 1 auto;
}
.formSection {
padding: 2rem;
background: var(--bg-color);
width: 400px;
border-radius: 10px;
}
.formSection h1 {
text-align: center;
color: black;
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,82 @@
import { App, AppSearch, Table, TableSearch } from "../definitions"
import getConfig from "next/config"
const { serverRuntimeConfig } = getConfig()
const apiKey = serverRuntimeConfig["apiKey"]
const appName = serverRuntimeConfig["appName"]
const host = serverRuntimeConfig["host"]
let APP: App | null = null
let TABLES: { [key: string]: Table } = {}
export async function makeCall(
method: string,
url: string,
opts?: { body?: any; appId?: string }
): Promise<any> {
const fetchOpts: any = {
method,
headers: {
"x-budibase-api-key": apiKey,
},
}
if (opts?.appId) {
fetchOpts.headers["x-budibase-app-id"] = opts.appId
}
if (opts?.body) {
fetchOpts.body =
typeof opts.body !== "string" ? JSON.stringify(opts.body) : opts.body
fetchOpts.headers["Content-Type"] = "application/json"
}
const finalUrl = `${host}/api/public/v1/${url}`
const response = await fetch(finalUrl, fetchOpts)
if (response.ok) {
return response.json()
} else {
const error = await response.text()
console.error("Budibase server error - ", error)
throw new Error(error)
}
}
export async function getApp(): Promise<App> {
if (APP) {
return APP
}
const apps: AppSearch = await makeCall("post", "applications/search", {
body: {
name: appName,
},
})
const app = apps.data.find((app: App) => app.name === appName)
if (!app) {
throw new Error(
"Could not find app, please make sure app name in config is correct."
)
}
APP = app
return app
}
export async function findTable(
appId: string,
tableName: string
): Promise<Table> {
if (TABLES[tableName]) {
return TABLES[tableName]
}
const tables: TableSearch = await makeCall("post", "tables/search", {
body: {
name: tableName,
},
appId,
})
const table = tables.data.find((table: Table) => table.name === tableName)
if (!table) {
throw new Error(
"Could not find table, please make sure your app is configured with the Postgres datasource correctly."
)
}
TABLES[tableName] = table
return table
}

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ All ports are BLOCKED except 22 (SSH), 80 (HTTP), 443 (HTTPS), and 10000
* Budibase website: http://budibase.com
For help and more information, visit https://docs.budibase.com/self-hosting/hosting-methods/digitalocean
For help and more information, visit https://docs.budibase.com/docs/digitalocean
********************************************************************************
To delete this message of the day: rm -rf $(readlink -f ${0})

View File

@ -5,7 +5,7 @@ version: "3"
services:
minio-service:
container_name: budi-minio-dev
restart: always
restart: on-failure
image: minio/minio
volumes:
- minio_data:/data
@ -23,7 +23,7 @@ services:
proxy-service:
container_name: budi-nginx-dev
restart: always
restart: on-failure
image: nginx:latest
volumes:
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
@ -38,7 +38,7 @@ services:
couchdb-service:
# platform: linux/amd64
container_name: budi-couchdb-dev
restart: always
restart: on-failure
image: ibmcom/couchdb3
environment:
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
@ -59,7 +59,7 @@ services:
redis-service:
container_name: budi-redis-dev
restart: always
restart: on-failure
image: redis
command: redis-server --requirepass ${REDIS_PASSWORD}
ports:

View File

@ -4,7 +4,7 @@ version: "3"
services:
app-service:
restart: always
restart: unless-stopped
image: budibase.docker.scarf.sh/budibase/apps
container_name: bbapps
environment:
@ -28,7 +28,7 @@ services:
- redis-service
worker-service:
restart: always
restart: unless-stopped
image: budibase.docker.scarf.sh/budibase/worker
container_name: bbworker
environment:
@ -53,7 +53,7 @@ services:
- couch-init
minio-service:
restart: always
restart: unless-stopped
image: minio/minio
volumes:
- minio_data:/data
@ -69,7 +69,7 @@ services:
retries: 3
proxy-service:
restart: always
restart: unless-stopped
ports:
- "${MAIN_PORT}:10000"
container_name: bbproxy
@ -81,7 +81,7 @@ services:
- couchdb-service
couchdb-service:
restart: always
restart: unless-stopped
image: ibmcom/couchdb3
environment:
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
@ -98,13 +98,14 @@ services:
command: ["sh","-c","sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;"]
redis-service:
restart: always
restart: unless-stopped
image: redis
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
watchtower-service:
restart: always
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
@ -116,7 +117,6 @@ services:
labels:
- "com.centurylinklabs.watchtower.enable=false"
volumes:
couchdb3_data:
driver: local

View File

@ -52,9 +52,8 @@ http {
proxy_pass http://{{ address }}:4001;
}
location /app/ {
location /app {
proxy_pass http://{{ address }}:4001;
rewrite ^/app/(.*)$ /$1 break;
}
location /builder {
@ -76,6 +75,7 @@ http {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_connect_timeout 300;
proxy_http_version 1.1;

View File

@ -19,18 +19,11 @@ http {
tcp_nodelay on;
server_tokens off;
types_hash_max_size 2048;
{{#if compose}}
resolver 127.0.0.11 ipv6=off;
{{/if}}
{{#if k8s}}
resolver kube-dns.kube-system.svc.cluster.local valid=10s;
{{/if}}
resolver {{ resolver }} valid=10s ipv6=off;
# buffering
client_body_buffer_size 1K;
client_header_buffer_size 1k;
client_max_body_size 10M;
client_max_body_size 20M;
ignore_invalid_headers off;
proxy_buffering off;
@ -49,13 +42,25 @@ http {
client_max_body_size 1000m;
ignore_invalid_headers off;
proxy_buffering off;
# port_in_redirect off;
set $csp_default "default-src 'self'";
set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io";
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
set $csp_object "object-src 'none'";
set $csp_base_uri "base-uri 'self'";
set $csp_connect "connect-src 'self' https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com";
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
set $csp_frame "frame-src 'self' https:";
set $csp_img "img-src http: https: data: blob:";
set $csp_manifest "manifest-src 'self'";
set $csp_media "media-src 'self' https://js.intercomcdn.com";
set $csp_worker "worker-src 'none'";
# Security Headers
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' data https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com; frame-src 'self'; img-src http: https: data; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
add_header Content-Security-Policy "${csp_default}; ${csp_script}; ${csp_style}; ${csp_object}; ${csp_base_uri}; ${csp_connect}; ${csp_font}; ${csp_frame}; ${csp_img}; ${csp_manifest}; ${csp_media}; ${csp_worker};" always;
# upstreams
set $apps {{ apps }};
@ -68,7 +73,6 @@ http {
location /app {
proxy_pass http://$apps:4002;
rewrite ^/app/(.*)$ /$1 break;
}
location = / {

View File

@ -0,0 +1,94 @@
{
"version": "2",
"templates": [
{
"type": 3,
"title": "Budibase",
"categories": ["Tools"],
"description": "Build modern business apps in minutes",
"logo": "https://budibase.com/favicon.ico",
"platform": "linux",
"repository": {
"url": "https://github.com/Budibase/budibase",
"stackfile": "hosting/docker-compose.yaml"
},
"env": [
{
"name": "MAIN_PORT",
"label": "Main port",
"default": "10000"
},
{
"name": "JWT_SECRET",
"label": "JWT secret",
"default": "change-me"
},
{
"name": "MINIO_ACCESS_KEY",
"label": "MinIO access key",
"default": "change-me"
},
{
"name": "MINIO_SECRET_KEY",
"label": "MinIO secret key",
"default": "change-me"
},
{
"name": "COUCH_DB_USER",
"default": "budibase",
"preset": true
},
{
"name": "COUCH_DB_PASSWORD",
"label": "Couch DB password",
"default": "change-me"
},
{
"name": "REDIS_PASSWORD",
"label": "Redis password",
"default": "change-me"
},
{
"name": "INTERNAL_API_KEY",
"label": "Internal API key",
"default": "change-me"
},
{
"name": "APP_PORT",
"default": "4002",
"preset": true
},
{
"name": "WORKER_PORT",
"default": "4003",
"preset": true
},
{
"name": "MINIO_PORT",
"default": "4004",
"preset": true
},
{
"name": "COUCH_DB_PORT",
"default": "4005",
"preset": true
},
{
"name": "REDIS_PORT",
"default": "6379",
"preset": true
},
{
"name": "WATCHTOWER_PORT",
"default": "6161",
"preset": true
},
{
"name": "BUDIBASE_ENVIRONMENT",
"default": "PRODUCTION",
"preset": true
}
]
}
]
}

View File

@ -1,145 +0,0 @@
user nginx;
error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 33282;
events {
worker_connections 1024;
}
http {
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
include /etc/nginx/mime.types;
default_type application/octet-stream;
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
types_hash_max_size 2048;
# buffering
client_body_buffer_size 1K;
client_header_buffer_size 1k;
client_max_body_size 1k;
ignore_invalid_headers off;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
map $http_upgrade $connection_upgrade {
default "upgrade";
}
server {
listen 10000 default_server;
listen [::]:10000 default_server;
server_name _;
client_max_body_size 1000m;
ignore_invalid_headers off;
proxy_buffering off;
port_in_redirect off;
# Security Headers
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me; frame-src 'self'; img-src http: https: data:; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
location /app {
proxy_pass http://app-service:4002;
rewrite ^/app/(.*)$ /$1 break;
}
location = / {
port_in_redirect off;
proxy_pass http://app-service:4002;
}
location = /v1/update {
proxy_pass http://watchtower-service:8080;
}
location /builder/ {
port_in_redirect off;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app-service:4002;
}
location ~ ^/(builder|app_) {
port_in_redirect off;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app-service:4002;
}
location ~ ^/api/(system|admin|global)/ {
proxy_pass http://worker-service:4003;
}
location /worker/ {
proxy_pass http://worker-service:4003;
rewrite ^/worker/(.*)$ /$1 break;
}
location /api/ {
# calls to the API are rate limited with bursting
limit_req zone=ratelimit burst=20 nodelay;
# 120s timeout on API requests
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app-service:4002;
}
location /db/ {
proxy_pass http://couchdb-service:5984;
rewrite ^/db/(.*)$ /$1 break;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://minio-service:9000;
}
client_header_timeout 60;
client_body_timeout 60;
keepalive_timeout 60;
# gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
}
}

97
hosting/single/Dockerfile Normal file
View File

@ -0,0 +1,97 @@
FROM couchdb
ENV COUCHDB_PASSWORD=budibase
ENV COUCHDB_USER=budibase
ENV COUCH_DB_URL=http://budibase:budibase@localhost:5984
ENV BUDIBASE_ENVIRONMENT=PRODUCTION
ENV MINIO_URL=http://localhost:9000
ENV REDIS_URL=localhost:6379
ENV WORKER_URL=http://localhost:4002
ENV INTERNAL_API_KEY=budibase
ENV JWT_SECRET=testsecret
ENV MINIO_ACCESS_KEY=budibase
ENV MINIO_SECRET_KEY=budibase
ENV SELF_HOSTED=1
ENV CLUSTER_PORT=10000
ENV REDIS_PASSWORD=budibase
ENV ARCHITECTURE=amd
ENV APP_PORT=4001
ENV WORKER_PORT=4002
RUN apt-get update
RUN apt-get install software-properties-common wget nginx -y
RUN apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main'
RUN apt-get update
# setup nginx
ADD hosting/single/nginx.conf /etc/nginx
RUN mkdir /etc/nginx/logs
RUN useradd www
RUN touch /etc/nginx/logs/error.log
RUN touch /etc/nginx/logs/nginx.pid
# install java
RUN apt-get install openjdk-8-jdk -y
# setup nodejs
WORKDIR /nodejs
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh
RUN bash /tmp/nodesource_setup.sh
RUN apt-get install nodejs
RUN npm install --global yarn
RUN npm install --global pm2
# setup redis
RUN apt install redis-server -y
# setup server
WORKDIR /app
ADD packages/server .
RUN ls -al
RUN yarn
RUN yarn build
# Install client for oracle datasource
RUN apt-get install unzip libaio1
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
# setup worker
WORKDIR /worker
ADD packages/worker .
RUN yarn
RUN yarn build
# setup clouseau
WORKDIR /
RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip
RUN unzip clouseau-2.21.0-dist.zip
RUN mv clouseau-2.21.0 /opt/clouseau
RUN rm clouseau-2.21.0-dist.zip
WORKDIR /opt/clouseau
RUN mkdir ./bin
ADD hosting/single/clouseau ./bin/
ADD hosting/single/log4j.properties .
ADD hosting/single/clouseau.ini .
RUN chmod +x ./bin/clouseau
# setup CouchDB
WORKDIR /opt/couchdb
ADD hosting/single/vm.args ./etc/
# setup minio
WORKDIR /minio
RUN wget https://dl.min.io/server/minio/release/linux-${ARCHITECTURE}64/minio
RUN chmod +x minio
# setup runner file
WORKDIR /
ADD hosting/single/runner.sh .
RUN chmod +x ./runner.sh
EXPOSE 10000
VOLUME /opt/couchdb/data
VOLUME /minio
# must set this just before running
ENV NODE_ENV=production
CMD ["./runner.sh"]

12
hosting/single/clouseau Normal file
View File

@ -0,0 +1,12 @@
#!/bin/sh
/usr/bin/java -server \
-Xmx2G \
-Dsun.net.inetaddr.ttl=30 \
-Dsun.net.inetaddr.negative.ttl=30 \
-Dlog4j.configuration=file:/opt/clouseau/log4j.properties \
-XX:OnOutOfMemoryError="kill -9 %p" \
-XX:+UseConcMarkSweepGC \
-XX:+CMSParallelRemarkEnabled \
-classpath '/opt/clouseau/*' \
com.cloudant.clouseau.Main \
/opt/clouseau/clouseau.ini

View File

@ -0,0 +1,13 @@
[clouseau]
; the name of the Erlang node created by the service, leave this unchanged
name=clouseau@127.0.0.1
; set this to the same distributed Erlang cookie used by the CouchDB nodes
cookie=monster
; the path where you would like to store the search index files
dir=/opt/couchdb/data/search
; the number of search indexes that can be open simultaneously
max_indexes_open=500

View File

@ -0,0 +1,4 @@
log4j.rootLogger=debug, CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %c [%p] %m%n

116
hosting/single/nginx.conf Normal file
View File

@ -0,0 +1,116 @@
user www www;
error_log /etc/nginx/logs/error.log;
pid /etc/nginx/logs/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 8192;
events {
worker_connections 1024;
}
http {
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
proxy_set_header Host $host;
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
types_hash_max_size 2048;
# buffering
client_header_buffer_size 1k;
client_max_body_size 20M;
ignore_invalid_headers off;
proxy_buffering off;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
map $http_upgrade $connection_upgrade {
default "upgrade";
}
server {
listen 10000 default_server;
listen [::]:10000 default_server;
server_name _;
client_max_body_size 1000m;
ignore_invalid_headers off;
proxy_buffering off;
# port_in_redirect off;
location /app {
proxy_pass http://127.0.0.1:4001;
}
location = / {
proxy_pass http://127.0.0.1:4001;
}
location ~ ^/(builder|app_) {
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:4001;
}
location ~ ^/api/(system|admin|global)/ {
proxy_pass http://127.0.0.1:4002;
}
location /worker/ {
proxy_pass http://127.0.0.1:4002;
rewrite ^/worker/(.*)$ /$1 break;
}
location /api/ {
# calls to the API are rate limited with bursting
limit_req zone=ratelimit burst=20 nodelay;
# 120s timeout on API requests
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:4001;
}
location /db/ {
proxy_pass http://127.0.0.1:5984;
rewrite ^/db/(.*)$ /$1 break;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://127.0.0.1:9000;
}
client_header_timeout 60;
client_body_timeout 60;
keepalive_timeout 60;
# gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
}
}

16
hosting/single/runner.sh Normal file
View File

@ -0,0 +1,16 @@
redis-server --requirepass $REDIS_PASSWORD &
/opt/clouseau/bin/clouseau &
/minio/minio server /minio &
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
/etc/init.d/nginx restart
pushd app
pm2 start --name app "yarn run:docker"
popd
pushd worker
pm2 start --name worker "yarn run:docker"
popd
sleep 10
URL=http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984
curl -X PUT ${URL}/_users
curl -X PUT ${URL}/_replicator
sleep infinity

32
hosting/single/vm.args Normal file
View File

@ -0,0 +1,32 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
# erlang cookie for clouseau security
-name couchdb@127.0.0.1
-setcookie monster
# Ensure that the Erlang VM listens on a known port
-kernel inet_dist_listen_min 9100
-kernel inet_dist_listen_max 9100
# Tell kernel and SASL not to log anything
-kernel error_logger silent
-sasl sasl_error_logger false
# Use kernel poll functionality if supported by emulator
+K true
# Start a pool of asynchronous IO threads
+A 16
# Comment this line out to enable the interactive Erlang shell on startup
+Bd -noinput

View File

@ -39,7 +39,7 @@
</p>
<h3 align="center">
<a href="https://docs.budibase.com/getting-started">Los Geht's</a>
<a href="https://docs.budibase.com/docs/quickstart-tutorials">Los Geht's</a>
<span> · </span>
<a href="https://docs.budibase.com">Dokumentation</a>
<span> · </span>
@ -109,7 +109,7 @@ $ budi hosting --start
4. Lege einen Admin-Benutzer an.
Gib die E-Mail und das Passwort für den neuen Admin-Benutzer ein.
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/getting-started).
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/docs/quickstart-tutorials).
<br />

View File

@ -112,7 +112,7 @@ The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps be
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/docs/hosting-methods).
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb&region=nyc1&refcode=0caaa6085a82&image=budibase-20-04)

View File

@ -1,5 +1,5 @@
{
"version": "1.0.76-alpha.3",
"version": "1.0.105-alpha.4",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -15,7 +15,9 @@
"prettier-plugin-svelte": "^2.3.0",
"rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0",
"svelte": "^3.38.2"
"svelte": "^3.38.2",
"@typescript-eslint/parser": "4.28.0",
"typescript": "4.5.5"
},
"scripts": {
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
@ -31,28 +33,33 @@
"nuke:docker": "lerna run --parallel dev:stack:nuke",
"clean": "lerna clean",
"kill-port": "kill-port 4001",
"dev": "yarn run kill-port && lerna link && lerna run --parallel dev:builder --concurrency 1",
"dev:noserver": "lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
"kill-builder": "kill-port 3000",
"kill-server": "kill-port 4001 4002",
"kill-all": "yarn run kill-builder && yarn run kill-server",
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
"test": "lerna run test",
"lint:eslint": "eslint packages",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier",
"lint:fix:eslint": "eslint --fix packages",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\"",
"lint:fix:ts": "lerna run lint:fix",
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"test:e2e": "lerna run cy:test",
"test:e2e:ci": "lerna run cy:ci",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"test:e2e": "lerna run cy:test --stream",
"test:e2e:ci": "lerna run cy:ci --stream",
"build:specs": "lerna run specs",
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
"build:docker:proxy:compose": "lerna run generate:proxy:compose && npm run build:docker:proxy",
"build:docker:proxy:preprod": "lerna run generate:proxy:preprod && npm run build:docker:proxy",
"build:docker:proxy:prod": "lerna run generate:proxy:prod && npm run build:docker:proxy",
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
"build:docker:proxy:prod": "node scripts/proxy/generateProxyConfig prod && npm run build:docker:proxy",
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
"build:docker:single": "lerna run build && lerna run predocker && npm run build:docker:single:image",
"build:docs": "lerna run build:docs",
"release:helm": "node scripts/releaseHelmChart",
"env:multi:enable": "lerna run env:multi:enable",

View File

@ -0,0 +1 @@
module.exports = require("./src/security/encryption")

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.76-alpha.3",
"version": "1.0.105-alpha.4",
"description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js",
"author": "Budibase",

View File

@ -32,11 +32,10 @@ const populateFromDB = async (userId, tenantId) => {
* @param {*} populateUser function to provide the user for re-caching. default to couch db
* @returns
*/
exports.getUser = async (
userId,
tenantId = null,
populateUser = populateFromDB
) => {
exports.getUser = async (userId, tenantId = null, populateUser = null) => {
if (!populateUser) {
populateUser = populateFromDB
}
if (!tenantId) {
try {
tenantId = getTenantId()

View File

@ -22,3 +22,18 @@ exports.getAccount = async email => {
return json[0]
}
exports.getStatus = async () => {
const response = await api.get(`/api/status`, {
headers: {
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY,
},
})
const json = await response.json()
if (response.status !== 200) {
throw new Error(`Error getting status`)
}
return json
}

View File

@ -1,5 +1,6 @@
const env = require("../environment")
const { Headers } = require("../../constants")
const { SEPARATOR, DocumentTypes } = require("../db/constants")
const cls = require("./FunctionContext")
const { getCouch } = require("../db")
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
@ -42,8 +43,39 @@ exports.doInTenant = (tenantId, task) => {
})
}
/**
* Given an app ID this will attempt to retrieve the tenant ID from it.
* @return {null|string} The tenant ID found within the app ID.
*/
exports.getTenantIDFromAppID = appId => {
if (!appId) {
return null
}
const split = appId.split(SEPARATOR)
const hasDev = split[1] === DocumentTypes.DEV
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
return null
}
if (hasDev) {
return split[2]
} else {
return split[1]
}
}
const setAppTenantId = appId => {
const appTenantId = this.getTenantIDFromAppID(appId) || this.DEFAULT_TENANT_ID
this.updateTenantId(appTenantId)
}
exports.doInAppContext = (appId, task) => {
if (!appId) {
throw new Error("appId is required")
}
return cls.run(() => {
// set the app tenant id
setAppTenantId(appId)
// set the app ID
cls.setOnContext(ContextKeys.APP_ID, appId)

View File

@ -14,6 +14,7 @@ exports.DocumentTypes = {
APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`,
ROLE: "role",
MIGRATIONS: "migrations",
DEV_INFO: "devinfo",
}
exports.StaticDatabases = {

View File

@ -9,11 +9,7 @@ const {
APP_PREFIX,
APP_DEV,
} = require("./constants")
const {
getTenantId,
getTenantIDFromAppID,
getGlobalDBName,
} = require("../tenancy")
const { getTenantId, getGlobalDBName } = require("../tenancy")
const fetch = require("node-fetch")
const { getCouch } = require("./index")
const { getAppMetadata } = require("../cache/appMetadata")
@ -30,6 +26,7 @@ const UNICODE_MAX = "\ufff0"
exports.ViewNames = {
USER_BY_EMAIL: "by_email",
BY_API_KEY: "by_api_key",
}
exports.StaticDatabases = StaticDatabases
@ -38,7 +35,6 @@ exports.DocumentTypes = DocumentTypes
exports.APP_PREFIX = APP_PREFIX
exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV
exports.SEPARATOR = SEPARATOR
exports.getTenantIDFromAppID = getTenantIDFromAppID
exports.isDevApp = isDevApp
exports.isProdAppID = isProdAppID
exports.isDevAppID = isDevAppID
@ -67,6 +63,7 @@ function getDocParams(docType, docId = null, otherProps = {}) {
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
}
}
exports.getDocParams = getDocParams
/**
* Generates a new workspace ID.
@ -339,6 +336,14 @@ const getConfigParams = ({ type, workspace, user }, otherProps = {}) => {
}
}
/**
* Generates a new dev info document ID - this is scoped to a user.
* @returns {string} The new dev info ID which info for dev (like api key) can be stored under.
*/
const generateDevInfoID = userId => {
return `${DocumentTypes.DEV_INFO}${SEPARATOR}${userId}`
}
/**
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
* @param {Object} db - db instance to query
@ -454,3 +459,4 @@ exports.generateConfigID = generateConfigID
exports.getConfigParams = getConfigParams
exports.getScopedFullConfig = getScopedFullConfig
exports.generateNewUsageQuotaDoc = generateNewUsageQuotaDoc
exports.generateDevInfoID = generateDevInfoID

View File

@ -1,4 +1,5 @@
const { DocumentTypes, ViewNames } = require("./utils")
const { getGlobalDB } = require("../tenancy")
function DesignDoc() {
return {
@ -9,7 +10,8 @@ function DesignDoc() {
}
}
exports.createUserEmailView = async db => {
exports.createUserEmailView = async () => {
const db = getGlobalDB()
let designDoc
try {
designDoc = await db.get("_design/database")
@ -31,3 +33,51 @@ exports.createUserEmailView = async db => {
}
await db.put(designDoc)
}
exports.createApiKeyView = async () => {
const db = getGlobalDB()
let designDoc
try {
designDoc = await db.get("_design/database")
} catch (err) {
designDoc = DesignDoc()
}
const view = {
map: `function(doc) {
if (doc._id.startsWith("${DocumentTypes.DEV_INFO}") && doc.apiKey) {
emit(doc.apiKey, doc.userId)
}
}`,
}
designDoc.views = {
...designDoc.views,
[ViewNames.BY_API_KEY]: view,
}
await db.put(designDoc)
}
exports.queryGlobalView = async (viewName, params, db = null) => {
const CreateFuncByName = {
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
}
// can pass DB in if working with something specific
if (!db) {
db = getGlobalDB()
}
try {
let response = (await db.query(`database/${viewName}`, params)).rows
response = response.map(resp =>
params.include_docs ? resp.doc : resp.value
)
return response.length <= 1 ? response[0] : response
} catch (err) {
if (err != null && err.name === "not_found") {
const createFunc = CreateFuncByName[viewName]
await createFunc()
return exports.queryGlobalView(viewName, params)
} else {
throw err
}
}
}

View File

@ -1,27 +0,0 @@
const {
isMultiTenant,
updateTenantId,
isTenantIdSet,
DEFAULT_TENANT_ID,
updateAppId,
} = require("../tenancy")
const ContextFactory = require("../context/FunctionContext")
const { getTenantIDFromAppID } = require("../db/utils")
module.exports = () => {
return ContextFactory.getMiddleware(ctx => {
// if not in multi-tenancy mode make sure its default and exit
if (!isMultiTenant()) {
updateTenantId(DEFAULT_TENANT_ID)
return
}
// if tenant ID already set no need to continue
if (isTenantIdSet()) {
return
}
const appId = ctx.appId ? ctx.appId : ctx.user ? ctx.user.appId : null
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
updateTenantId(tenantId)
updateAppId(appId)
})
}

View File

@ -4,6 +4,9 @@ const { getUser } = require("../cache/user")
const { getSession, updateSessionTTL } = require("../security/sessions")
const { buildMatcherRegex, matches } = require("./matchers")
const env = require("../environment")
const { SEPARATOR, ViewNames, queryGlobalView } = require("../../db")
const { getGlobalDB, doInTenant } = require("../tenancy")
const { decrypt } = require("../security/encryption")
function finalise(
ctx,
@ -16,6 +19,33 @@ function finalise(
ctx.version = version
}
async function checkApiKey(apiKey, populateUser) {
if (apiKey === env.INTERNAL_API_KEY) {
return { valid: true }
}
const decrypted = decrypt(apiKey)
const tenantId = decrypted.split(SEPARATOR)[0]
return doInTenant(tenantId, async () => {
const db = getGlobalDB()
// api key is encrypted in the database
const userId = await queryGlobalView(
ViewNames.BY_API_KEY,
{
key: apiKey,
},
db
)
if (userId) {
return {
valid: true,
user: await getUser(userId, tenantId, populateUser),
}
} else {
throw "Invalid API key"
}
})
}
/**
* This middleware is tenancy aware, so that it does not depend on other middlewares being used.
* The tenancy modules should not be used here and it should be assumed that the tenancy context
@ -79,9 +109,19 @@ module.exports = (
const apiKey = ctx.request.headers[Headers.API_KEY]
const tenantId = ctx.request.headers[Headers.TENANT_ID]
// this is an internal request, no user made it
if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) {
authenticated = true
internal = true
if (!authenticated && apiKey) {
const populateUser = opts.populateUser ? opts.populateUser(ctx) : null
const { valid, user: foundUser } = await checkApiKey(
apiKey,
populateUser
)
if (valid && foundUser) {
authenticated = true
user = foundUser
} else if (valid) {
authenticated = true
internal = true
}
}
if (!user && tenantId) {
user = { tenantId }
@ -101,6 +141,7 @@ module.exports = (
// allow configuring for public access
if ((opts && opts.publicAllowed) || publicEndpoint) {
finalise(ctx, { authenticated: false, version, publicEndpoint })
return next()
} else {
ctx.throw(err.status || 403, err)
}

View File

@ -6,7 +6,6 @@ const { authError } = require("./passport/utils")
const authenticated = require("./authenticated")
const auditLog = require("./auditLog")
const tenancy = require("./tenancy")
const appTenancy = require("./appTenancy")
const internalApi = require("./internalApi")
const datasourceGoogle = require("./passport/datasource/google")
const csrf = require("./csrf")
@ -19,7 +18,6 @@ module.exports = {
authenticated,
auditLog,
tenancy,
appTenancy,
authError,
internalApi,
datasource: {

View File

@ -1,15 +1,29 @@
const google = require("../google")
const { Cookies } = require("../../../constants")
const { Cookies, Configs } = require("../../../constants")
const { clearCookie, getCookie } = require("../../../utils")
const { getDB } = require("../../../db")
const { getScopedConfig } = require("../../../db/utils")
const environment = require("../../../environment")
const { getGlobalDB } = require("../../../tenancy")
async function preAuth(passport, ctx, next) {
// get the relevant config
const googleConfig = {
async function fetchGoogleCreds() {
// try and get the config from the tenant
const db = getGlobalDB()
const googleConfig = await getScopedConfig(db, {
type: Configs.GOOGLE,
})
// or fall back to env variables
const config = googleConfig || {
clientID: environment.GOOGLE_CLIENT_ID,
clientSecret: environment.GOOGLE_CLIENT_SECRET,
}
return config
}
async function preAuth(passport, ctx, next) {
// get the relevant config
const googleConfig = await fetchGoogleCreds()
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
@ -26,10 +40,7 @@ async function preAuth(passport, ctx, next) {
async function postAuth(passport, ctx, next) {
// get the relevant config
const config = {
clientID: environment.GOOGLE_CLIENT_ID,
clientSecret: environment.GOOGLE_CLIENT_SECRET,
}
const config = await fetchGoogleCreds()
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
const strategy = await google.strategyFactory(

View File

@ -51,7 +51,10 @@ exports.strategyFactory = async function (
)
} catch (err) {
console.error(err)
throw new Error("Error constructing google authentication strategy", err)
throw new Error(
`Error constructing google authentication strategy: ${err}`,
err
)
}
}
// expose for testing

View File

@ -78,6 +78,7 @@ exports.ObjectStore = bucket => {
const config = {
s3ForcePathStyle: true,
signatureVersion: "v4",
apiVersion: "2006-03-01",
params: {
Bucket: sanitizeBucket(bucket),
},
@ -102,17 +103,21 @@ exports.makeSureBucketExists = async (client, bucketName) => {
.promise()
} catch (err) {
const promises = STATE.bucketCreationPromises
const doesntExist = err.statusCode === 404,
noAccess = err.statusCode === 403
if (promises[bucketName]) {
await promises[bucketName]
} else if (err.statusCode === 404) {
// bucket doesn't exist create it
promises[bucketName] = client
.createBucket({
Bucket: bucketName,
})
.promise()
await promises[bucketName]
delete promises[bucketName]
} else if (doesntExist || noAccess) {
if (doesntExist) {
// bucket doesn't exist create it
promises[bucketName] = client
.createBucket({
Bucket: bucketName,
})
.promise()
await promises[bucketName]
delete promises[bucketName]
}
// public buckets are quite hidden in the system, make sure
// no bucket is set accidentally
if (PUBLIC_BUCKETS.includes(bucketName)) {
@ -124,7 +129,7 @@ exports.makeSureBucketExists = async (client, bucketName) => {
.promise()
}
} else {
throw err
throw new Error("Unable to write to object store bucket.")
}
}
}

View File

@ -22,12 +22,25 @@ exports.Databases = {
exports.SEPARATOR = SEPARATOR
exports.getRedisOptions = (clustered = false) => {
const [host, port, ...rest] = REDIS_URL.split(":")
let password = REDIS_PASSWORD
let url = REDIS_URL.split("//")
// get rid of the protocol
url = url.length > 1 ? url[1] : url[0]
// check for a password etc
url = url.split("@")
if (url.length > 1) {
// get the password
password = url[0].split(":")[1]
url = url[1]
} else {
url = url[0]
}
const [host, port] = url.split(":")
let redisProtocolUrl
// fully qualified redis URL
if (rest.length && /rediss?/.test(host)) {
if (/rediss?:\/\//.test(REDIS_URL)) {
redisProtocolUrl = REDIS_URL
}
@ -37,13 +50,13 @@ exports.getRedisOptions = (clustered = false) => {
if (clustered) {
opts.redisOptions = {}
opts.redisOptions.tls = {}
opts.redisOptions.password = REDIS_PASSWORD
opts.redisOptions.password = password
opts.slotsRefreshTimeout = SLOT_REFRESH_MS
opts.dnsLookup = (address, callback) => callback(null, address)
} else {
opts.host = host
opts.port = port
opts.password = REDIS_PASSWORD
opts.password = password
}
return { opts, host, port, redisProtocolUrl }
}

View File

@ -0,0 +1 @@
exports.lookupApiKey = async () => {}

View File

@ -0,0 +1,33 @@
const crypto = require("crypto")
const env = require("../environment")
const ALGO = "aes-256-ctr"
const SECRET = env.JWT_SECRET
const SEPARATOR = "-"
const ITERATIONS = 10000
const RANDOM_BYTES = 16
const STRETCH_LENGTH = 32
function stretchString(string, salt) {
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
}
exports.encrypt = input => {
const salt = crypto.randomBytes(RANDOM_BYTES)
const stretched = stretchString(SECRET, salt)
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
const base = cipher.update(input)
const final = cipher.final()
const encrypted = Buffer.concat([base, final]).toString("hex")
return `${salt.toString("hex")}${SEPARATOR}${encrypted}`
}
exports.decrypt = input => {
const [salt, encrypted] = input.split(SEPARATOR)
const saltBuffer = Buffer.from(salt, "hex")
const stretched = stretchString(SECRET, saltBuffer)
const decipher = crypto.createDecipheriv(ALGO, stretched, saltBuffer)
const base = decipher.update(Buffer.from(encrypted, "hex"))
const final = decipher.final()
return Buffer.concat([base, final]).toString()
}

View File

@ -10,6 +10,7 @@ const PermissionLevels = {
// these are the global types, that govern the underlying default behaviour
const PermissionTypes = {
APP: "app",
TABLE: "table",
USER: "user",
AUTOMATION: "automation",

View File

@ -1,5 +1,5 @@
const { cloneDeep } = require("lodash/fp")
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
const { BUILTIN_PERMISSION_IDS, PermissionLevels } = require("./permissions")
const {
generateRoleID,
getRoleParams,
@ -180,6 +180,20 @@ exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => {
return opts.idOnly ? roles.map(role => role._id) : roles
}
// this function checks that the provided permissions are in an array format
// some templates/older apps will use a simple string instead of array for roles
// convert the string to an array using the theory that write is higher than read
exports.checkForRoleResourceArray = (rolePerms, resourceId) => {
if (rolePerms && !Array.isArray(rolePerms[resourceId])) {
const permLevel = rolePerms[resourceId]
rolePerms[resourceId] = [permLevel]
if (permLevel === PermissionLevels.WRITE) {
rolePerms[resourceId].push(PermissionLevels.READ)
}
}
return rolePerms
}
/**
* Given an app ID this will retrieve all of the roles that are currently within that app.
* @return {Promise<object[]>} An array of the role objects that were found.
@ -209,15 +223,27 @@ exports.getAllRoles = async appId => {
roles.push(Object.assign(builtinRole, dbBuiltin))
}
}
// check permissions
for (let role of roles) {
if (!role.permissions) {
continue
}
for (let resourceId of Object.keys(role.permissions)) {
role.permissions = exports.checkForRoleResourceArray(
role.permissions,
resourceId
)
}
}
return roles
}
/**
* This retrieves the required role
* @param permLevel
* @param resourceId
* @param subResourceId
* @return {Promise<{permissions}|Object>}
* This retrieves the required role for a resource
* @param permLevel The level of request
* @param resourceId The resource being requested
* @param subResourceId The sub resource being requested
* @return {Promise<{permissions}|Object>} returns the permissions required to access.
*/
exports.getRequiredResourceRole = async (
permLevel,

View File

@ -1,6 +1,11 @@
const { getDB } = require("../db")
const { SEPARATOR, StaticDatabases, DocumentTypes } = require("../db/constants")
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("../context")
const { SEPARATOR, StaticDatabases } = require("../db/constants")
const {
getTenantId,
DEFAULT_TENANT_ID,
isMultiTenant,
getTenantIDFromAppID,
} = require("../context")
const env = require("../environment")
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
@ -118,26 +123,6 @@ exports.getTenantUser = async identifier => {
}
}
/**
* Given an app ID this will attempt to retrieve the tenant ID from it.
* @return {null|string} The tenant ID found within the app ID.
*/
exports.getTenantIDFromAppID = appId => {
if (!appId) {
return null
}
const split = appId.split(SEPARATOR)
const hasDev = split[1] === DocumentTypes.DEV
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
return null
}
if (hasDev) {
return split[2]
} else {
return split[1]
}
}
exports.isUserInAppTenant = (appId, user = null) => {
let userTenantId
if (user) {
@ -145,7 +130,7 @@ exports.isUserInAppTenant = (appId, user = null) => {
} else {
userTenantId = getTenantId()
}
const tenantId = exports.getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
return tenantId === userTenantId
}

View File

@ -3,10 +3,11 @@ const {
SEPARATOR,
ViewNames,
generateGlobalUserID,
getAllApps,
} = require("./db/utils")
const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt")
const { createUserEmailView } = require("./db/views")
const { queryGlobalView } = require("./db/views")
const { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants")
const {
getGlobalDB,
@ -20,8 +21,10 @@ const { hash } = require("./hashing")
const userCache = require("./cache/user")
const env = require("./environment")
const { getUserSessions, invalidateSessions } = require("./security/sessions")
const tenancy = require("./tenancy")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
const PROD_APP_PREFIX = "/app/"
function confirmAppId(possibleAppId) {
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
@ -29,16 +32,35 @@ function confirmAppId(possibleAppId) {
: undefined
}
async function resolveAppUrl(ctx) {
const appUrl = ctx.path.split("/")[2]
let possibleAppUrl = `/${appUrl.toLowerCase()}`
let tenantId = tenancy.getTenantId()
if (!env.SELF_HOSTED && ctx.subdomains.length) {
// always use the tenant id from the url in cloud
tenantId = ctx.subdomains[0]
}
// search prod apps for a url that matches
const apps = await tenancy.doInTenant(tenantId, () =>
getAllApps({ dev: false })
)
const app = apps.filter(
a => a.url && a.url.toLowerCase() === possibleAppUrl
)[0]
return app && app.appId ? app.appId : undefined
}
/**
* Given a request tries to find the appId, which can be located in various places
* @param {object} ctx The main request body to look through.
* @returns {string|undefined} If an appId was found it will be returned.
*/
exports.getAppId = ctx => {
const options = [ctx.headers[Headers.APP_ID], ctx.params.appId]
if (ctx.subdomains) {
options.push(ctx.subdomains[1])
}
exports.getAppIdFromCtx = async ctx => {
// look in headers
const options = [ctx.headers[Headers.APP_ID]]
let appId
for (let option of options) {
appId = confirmAppId(option)
@ -47,16 +69,24 @@ exports.getAppId = ctx => {
}
}
// look in body if can't find it in subdomain
// look in body
if (!appId && ctx.request.body && ctx.request.body.appId) {
appId = confirmAppId(ctx.request.body.appId)
}
// look in the url - dev app
let appPath =
ctx.request.headers.referrer ||
ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX))
if (!appId && appPath.length !== 0) {
if (!appId && appPath.length) {
appId = confirmAppId(appPath[0])
}
// look in the url - prod app
if (!appId && ctx.path.startsWith(PROD_APP_PREFIX)) {
appId = confirmAppId(await resolveAppUrl(ctx))
}
return appId
}
@ -139,25 +169,11 @@ exports.getGlobalUserByEmail = async email => {
if (email == null) {
throw "Must supply an email address to view"
}
const db = getGlobalDB()
try {
let users = (
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
key: email.toLowerCase(),
include_docs: true,
})
).rows
users = users.map(user => user.doc)
return users.length <= 1 ? users[0] : users
} catch (err) {
if (err != null && err.name === "not_found") {
await createUserEmailView(db)
return exports.getGlobalUserByEmail(email)
} else {
throw err
}
}
return queryGlobalView(ViewNames.USER_BY_EMAIL, {
key: email.toLowerCase(),
include_docs: true,
})
}
exports.saveUser = async (

View File

@ -3338,9 +3338,9 @@ mimic-fn@^2.1.0:
brace-expansion "^1.1.7"
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mixin-deep@^1.2.0:
version "1.3.2"

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "1.0.76-alpha.3",
"version": "1.0.105-alpha.4",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.72-alpha.0",
"@budibase/string-templates": "^1.0.105-alpha.4",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",

View File

@ -57,3 +57,10 @@
</div>
</div>
{/if}
<style>
.spectrum-Toast {
pointer-events: all;
width: 100%;
}
</style>

View File

@ -0,0 +1,31 @@
<script>
import "@spectrum-css/toast/dist/index-vars.css"
import Portal from "svelte-portal"
import { banner } from "../Stores/banner"
import Banner from "./Banner.svelte"
import { fly } from "svelte/transition"
</script>
<Portal target=".banner-container">
<div class="banner">
{#if $banner.message}
<div transition:fly={{ y: -30 }}>
<Banner
type={$banner.type}
extraButtonText={$banner.extraButtonText}
extraButtonAction={$banner.extraButtonAction}
on:change={$banner.onChange}
>
{$banner.message}
</Banner>
</div>
{/if}
</div>
</Portal>
<style>
.banner {
pointer-events: none;
width: 100%;
}
</style>

View File

@ -13,6 +13,7 @@
export let icon = undefined
export let active = false
export let tooltip = undefined
export let dataCy
let showTooltip = false
</script>
@ -27,6 +28,7 @@
class:active
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
{disabled}
data-cy={dataCy}
on:click|preventDefault
on:mouseover={() => (showTooltip = true)}
on:focus={() => (showTooltip = true)}

View File

@ -47,7 +47,9 @@
<use xlink:href="#spectrum-css-icon-Dash100" />
</svg>
</span>
<span class="spectrum-Checkbox-label">{text || ""}</span>
{#if text}
<span class="spectrum-Checkbox-label">{text}</span>
{/if}
</label>
<style>

View File

@ -19,18 +19,33 @@
const dispatch = createEventDispatcher()
const flatpickrId = `${uuid()}-wrapper`
let open = false
let flatpickr, flatpickrOptions, isTimeOnly
let flatpickr, flatpickrOptions
const resolveTimeStamp = timestamp => {
let maskedDate = new Date(`0-${timestamp}`)
if (maskedDate instanceof Date && !isNaN(maskedDate.getTime())) {
return maskedDate
} else {
return null
}
}
$: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly
$: flatpickrOptions = {
element: `#${flatpickrId}`,
enableTime: isTimeOnly || enableTime || false,
noCalendar: isTimeOnly || false,
enableTime: timeOnly || enableTime || false,
noCalendar: timeOnly || false,
altInput: true,
altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
wrap: true,
appendTo,
disableMobile: "true",
onReady: () => {
let timestamp = resolveTimeStamp(value)
if (timeOnly && timestamp) {
dispatch("change", timestamp.toISOString())
}
},
}
const handleChange = event => {
@ -39,10 +54,9 @@
if (newValue) {
newValue = newValue.toISOString()
}
// if time only set date component to today
// if time only set date component to 2000-01-01
if (timeOnly) {
const todayDate = new Date().toISOString().split("T")[0]
newValue = `${todayDate}T${newValue.split("T")[1]}`
newValue = `2000-01-01T${newValue.split("T")[1]}`
}
dispatch("change", newValue)
}
@ -76,10 +90,13 @@
return null
}
let date
let time = new Date(`0-${val}`)
let time
// it is a string like 00:00:00, just time
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
date = time
let ts = resolveTimeStamp(val)
if (timeOnly && ts) {
date = ts
} else if (val instanceof Date) {
// Use real date obj if already parsed
date = val
@ -101,7 +118,7 @@
}
</script>
{#key isTimeOnly}
{#key timeOnly}
<Flatpickr
bind:flatpickr
value={parseDate(value)}

View File

@ -54,34 +54,43 @@
<svelte:window on:keydown={handleKey} />
<!-- These svelte if statements need to be defined like this. -->
<!-- The modal transitions do not work if nested inside more than one "if" -->
{#if visible && inline}
<div use:focusFirstInput class="spectrum-Modal inline is-open">
<slot />
</div>
{:else if visible}
{#if inline}
{#if visible}
<div use:focusFirstInput class="spectrum-Modal inline is-open">
<slot />
</div>
{/if}
{:else}
<!--
We cannot conditionally render the portal as this leads to a missing
insertion point when using nested modals. Therefore we just conditionally
render the content of the portal.
It still breaks the modal animation, but its better than soft bricking the
screen.
-->
<Portal target=".modal-container">
<div
class="spectrum-Underlay is-open"
in:fade={{ duration: 200 }}
out:fade|local={{ duration: 200 }}
on:mousedown|self={cancel}
>
<div class="modal-wrapper" on:mousedown|self={cancel}>
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
<slot name="outside" />
<div
use:focusFirstInput
class="spectrum-Modal is-open"
in:fly={{ y: 30, duration: 200 }}
out:fly|local={{ y: 30, duration: 200 }}
>
<slot />
{#if visible}
<div
class="spectrum-Underlay is-open"
in:fade={{ duration: 200 }}
out:fade|local={{ duration: 200 }}
on:mousedown|self={cancel}
>
<div class="modal-wrapper" on:mousedown|self={cancel}>
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
<slot name="outside" />
<div
use:focusFirstInput
class="spectrum-Modal is-open"
in:fly={{ y: 30, duration: 200 }}
out:fly|local={{ y: 30, duration: 200 }}
>
<slot />
</div>
</div>
</div>
</div>
</div>
{/if}
</Portal>
{/if}

View File

@ -5,6 +5,7 @@
import Divider from "../Divider/Divider.svelte"
import Icon from "../Icon/Icon.svelte"
import Context from "../context"
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
export let title = undefined
export let size = "S"
@ -102,15 +103,22 @@
<Button group secondary on:click={close}>{cancelText}</Button>
{/if}
{#if showConfirmButton}
<Button
group
cta
{...$$restProps}
disabled={confirmDisabled}
on:click={confirm}
>
{confirmText}
</Button>
<span class="confirm-wrap">
<Button
group
cta
{...$$restProps}
disabled={confirmDisabled}
on:click={confirm}
>
{#if loading}
<ProgressCircle overBackground={true} size="S" />
{/if}
{#if !loading}
{confirmText}
{/if}
</Button>
</span>
{/if}
</div>
{/if}
@ -165,4 +173,12 @@
.secondary-action {
margin-right: auto;
}
.spectrum-Dialog-buttonGroup {
padding-left: 0;
}
.confirm-wrap :global(.spectrum-Button-label) {
display: contents;
}
</style>

View File

@ -0,0 +1,37 @@
import { writable } from "svelte/store"
export function createBannerStore() {
const DEFAULT_CONFIG = {}
const banner = writable(DEFAULT_CONFIG)
const show = async (
// eslint-disable-next-line
config = { message, type, extraButtonText, extraButtonAction, onChange }
) => {
banner.update(store => {
return {
...store,
...config,
}
})
}
const showStatus = async () => {
const config = {
message: "Some systems are experiencing issues",
type: "negative",
extraButtonText: "View Status",
extraButtonAction: () => window.open("https://status.budibase.com/"),
}
await show(config)
}
return {
subscribe: banner.subscribe,
showStatus,
}
}
export const banner = createBannerStore()

View File

@ -60,7 +60,7 @@ export const createNotificationStore = () => {
}
function id() {
return "_" + Math.random().toString(36).substr(2, 9)
return "_" + Math.random().toString(36).slice(2, 9)
}
export const notifications = createNotificationStore()

View File

@ -8,9 +8,21 @@
export let allowEditRows = false
</script>
{#if allowSelectRows}
<Checkbox value={selected} />
{/if}
{#if allowEditRows}
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton>
{/if}
<div>
{#if allowSelectRows}
<Checkbox value={selected} />
{/if}
{#if allowEditRows}
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton>
{/if}
</div>
<style>
div {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
}
</style>

View File

@ -5,6 +5,7 @@
import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep, deepGet } from "../helpers"
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
import Checkbox from "../Form/Checkbox.svelte"
/**
* The expected schema is our normal couch schemas for our tables.
@ -31,7 +32,6 @@
export let allowEditRows = true
export let allowEditColumns = true
export let selectedRows = []
export let editColumnTitle = "Edit"
export let customRenderers = []
export let disableSorting = false
export let autoSortColumns = true
@ -50,10 +50,13 @@
// Table state
let height = 0
let loaded = false
let checkboxStatus = false
$: schema = fixSchema(schema)
$: if (!loading) loaded = true
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
$: rows = fields?.length ? data || [] : []
$: totalRowCount = rows?.length || 0
$: visibleRowCount = getVisibleRowCount(
loaded,
height,
@ -61,12 +64,27 @@
rowCount,
rowHeight
)
$: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
$: heightStyle = getHeightStyle(
visibleRowCount,
rowCount,
totalRowCount,
rowHeight
)
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
$: showEditColumn = allowEditRows || allowSelectRows
$: cellStyles = computeCellStyles(schema)
// Deselect the "select all" checkbox when the user navigates to a new page
$: {
let checkRowCount = rows.filter(o1 =>
selectedRows.some(o2 => o1._id === o2._id)
)
if (checkRowCount.length === 0) {
checkboxStatus = false
}
}
const fixSchema = schema => {
let fixedSchema = {}
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
@ -95,11 +113,16 @@
return Math.min(allRows, Math.ceil(height / rowHeight))
}
const getContentStyle = (visibleRows, rowCount, rowHeight) => {
if (!rowCount || !visibleRows) {
const getHeightStyle = (
visibleRowCount,
rowCount,
totalRowCount,
rowHeight
) => {
if (!rowCount || !visibleRowCount || totalRowCount <= rowCount) {
return ""
}
return `height: ${headerHeight + visibleRows * rowHeight}px;`
return `height: ${headerHeight + visibleRowCount * rowHeight}px;`
}
const getGridStyle = (fields, schema, showEditColumn) => {
@ -197,13 +220,32 @@
if (!allowSelectRows) {
return
}
if (selectedRows.includes(row)) {
selectedRows = selectedRows.filter(selectedRow => selectedRow !== row)
if (selectedRows.some(selectedRow => selectedRow._id === row._id)) {
selectedRows = selectedRows.filter(
selectedRow => selectedRow._id !== row._id
)
} else {
selectedRows = [...selectedRows, row]
}
}
const toggleSelectAll = e => {
const select = !!e.detail
if (select) {
// Add any rows which are not already in selected rows
rows.forEach(row => {
if (selectedRows.findIndex(x => x._id === row._id) === -1) {
selectedRows.push(row)
}
})
} else {
// Remove any rows from selected rows that are in the current data set
selectedRows = selectedRows.filter(el =>
rows.every(f => f._id !== el._id)
)
}
}
const computeCellStyles = schema => {
let styles = {}
Object.keys(schema || {}).forEach(field => {
@ -233,18 +275,25 @@
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
>
{#if !loaded}
<div class="loading" style={contentStyle}>
<div class="loading" style={heightStyle}>
<ProgressCircle />
</div>
{:else}
<div class="spectrum-Table" style={`${contentStyle}${gridStyle}`}>
<div class="spectrum-Table" style={`${heightStyle}${gridStyle}`}>
{#if fields.length}
<div class="spectrum-Table-head">
{#if showEditColumn}
<div
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
>
{editColumnTitle || ""}
{#if allowSelectRows}
<Checkbox
bind:value={checkboxStatus}
on:change={toggleSelectAll}
/>
{:else}
Edit
{/if}
</div>
{/if}
{#each fields as field}
@ -302,11 +351,16 @@
{#if showEditColumn}
<div
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
on:click={e => {
toggleSelectRow(row)
e.stopPropagation()
}}
>
<SelectEditRenderer
data={row}
selected={selectedRows.includes(row)}
onToggleSelection={() => toggleSelectRow(row)}
selected={selectedRows.findIndex(
selectedRow => selectedRow._id === row._id
) !== -1}
onEdit={e => editRow(e, row)}
{allowSelectRows}
{allowEditRows}

View File

@ -68,7 +68,7 @@
})
function id() {
return "_" + Math.random().toString(36).substr(2, 9)
return "_" + Math.random().toString(36).slice(2, 9)
}
</script>

View File

@ -60,6 +60,7 @@ export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
export { default as Banner } from "./Banner/Banner.svelte"
export { default as BannerDisplay } from "./Banner/BannerDisplay.svelte"
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
export { default as RichTextField } from "./Form/RichTextField.svelte"
@ -84,6 +85,7 @@ export { default as clickOutside } from "./Actions/click_outside"
// Stores
export { notifications, createNotificationStore } from "./Stores/notifications"
export { banner } from "./Stores/banner"
// Helpers
export * as Helpers from "./helpers"

View File

@ -28,7 +28,7 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@budibase/handlebars-helpers@^0.11.7":
"@budibase/handlebars-helpers@^0.11.8":
version "0.11.8"
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.8.tgz#6953d29673a8c5c407e096c0a84890465c7ce841"
integrity sha512-ggWJUt0GqsHFAEup5tlWlcrmYML57nKhpNGGLzVsqXVYN8eVmf3xluYmmMe7fDYhQH0leSprrdEXmsdFQF3HAQ==
@ -53,12 +53,12 @@
to-gfm-code-block "^0.1.1"
year "^0.2.1"
"@budibase/string-templates@^1.0.72-alpha.0":
version "1.0.75"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.75.tgz#5b4061f1a626160ec092f32f036541376298100c"
integrity sha512-hPgr6n5cpSCGFEha5DS/P+rtRXOLc72M6y4J/scl59JvUi/ZUJkjRgJdpQPdBLu04CNKp89V59+rAqAuDjOC0g==
"@budibase/string-templates@^1.0.105-alpha.4":
version "1.0.108"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.108.tgz#14949560148ef11b6b385952ed5787c12b890f2c"
integrity sha512-7Tts91Dzy+A7OdObTIaBNAdaixC7wmabnNTWYqk1d6TM6H5yv++bd/a9gVdUM7ptsuMy7uoP/ZoTZZRhp3ozfA==
dependencies:
"@budibase/handlebars-helpers" "^0.11.7"
"@budibase/handlebars-helpers" "^0.11.8"
dayjs "^1.10.4"
handlebars "^4.7.6"
handlebars-utils "^1.0.6"
@ -2177,9 +2177,9 @@ minimatch@^3.0.4:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mixin-deep@^1.2.0:
version "1.3.2"
@ -3410,9 +3410,9 @@ typo-js@*:
integrity sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg==
uglify-js@^3.1.4:
version "3.15.1"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.1.tgz#9403dc6fa5695a6172a91bc983ea39f0f7c9086d"
integrity sha512-FAGKF12fWdkpvNJZENacOH0e/83eG6JyVQyanIJaBXCN1J11TUQv1T1/z8S+Z0CG0ZPk1nPcreF/c7lrTd0TEQ==
version "3.15.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.3.tgz#9aa82ca22419ba4c0137642ba0df800cb06e0471"
integrity sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg==
unbox-primitive@^1.0.0:
version "1.0.1"
@ -3503,9 +3503,9 @@ vendors@^1.0.0:
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
vm2@^3.9.4:
version "3.9.8"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.8.tgz#e99c000db042735cd2f94d8db6c42163a17be04e"
integrity sha512-/1PYg/BwdKzMPo8maOZ0heT7DLI0DAFTm7YQaz/Lim9oIaFZsJs3EdtalvXuBfZwczNwsYhju75NW4d6E+4q+w==
version "3.9.9"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.9.tgz#c0507bc5fbb99388fad837d228badaaeb499ddc5"
integrity sha512-xwTm7NLh/uOjARRBs8/95H0e8fT3Ukw5D/JJWhxMbhKzNh1Nu981jQKvkep9iKYNxzlVrdzD0mlBGkDKZWprlw==
dependencies:
acorn "^8.7.0"
acorn-walk "^8.2.0"

View File

@ -1,9 +1,10 @@
{
"baseUrl": "http://localhost:10001",
"baseUrl": "http://localhost:4100",
"video": false,
"projectId": "bmbemn",
"env": {
"PORT": "10001",
"PORT": "4100",
"WORKER_PORT": "4200",
"JWT_SECRET": "test",
"HOST_IP": ""
}

View File

@ -8,7 +8,7 @@ filterTests(['all'], () => {
it("should change the icon and colour for an application", () => {
// Search for test application
cy.searchForApplication("Cypress Tests")
cy.applicationInAppTable("Cypress Tests")
cy.get(".appTable")
.within(() => {
cy.get(".spectrum-Icon").eq(1).click()

View File

@ -2,11 +2,164 @@ import filterTests from '../support/filterTests'
filterTests(['smoke', 'all'], () => {
context("Create an Application", () => {
it("should create a new application", () => {
before(() => {
cy.login()
cy.createTestApp()
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.contains("Cypress Tests").should("exist")
cy.deleteApp("Cypress Tests")
})
if (!(Cypress.env("TEST_ENV"))) {
it("should show the new user UI/UX", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(`[data-cy="create-app-btn"]`).contains('Start from scratch').should("exist")
cy.get(`[data-cy="import-app-btn"]`).should("exist")
cy.get(".template-category-filters").should("exist")
cy.get(".template-categories").should("exist")
cy.get(".appTable").should("not.exist")
})
}
it("should provide filterable templates", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500)
if (Cypress.env("TEST_ENV")) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
cy.get(".template-category-filters").should("exist")
cy.get(".template-categories").should("exist")
cy.get(".template-category").its('length').should('be.gt', 1)
cy.get(".template-category-filters .spectrum-ActionButton").its('length').should('be.gt', 2)
cy.get(".template-category-filters .spectrum-ActionButton").eq(1).click()
cy.get(".template-category").should('have.length', 1)
cy.get(".template-category-filters .spectrum-ActionButton").eq(0).click()
cy.get(".template-category").its('length').should('be.gt', 1)
})
it("should enforce a valid url before submission", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500)
// Start create app process. If apps already exist, click second button
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
}
})
const appName = "Cypress Tests"
cy.get(".spectrum-Modal").within(() => {
//Auto fill
cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
cy.get("input").eq(1).should("have.value", "/cypress-tests")
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled')
//Empty the app url - disabled create
cy.get("input").eq(1).clear().blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled')
//Invalid url
cy.get("input").eq(1).type("/new app-url").blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled')
//Specifically alter the url
cy.get("input").eq(1).clear()
cy.get("input").eq(1).type("another-app-name").blur()
cy.get("input").eq(1).should("have.value", "/another-app-name")
cy.get("input").eq(0).should("have.value", appName)
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled')
})
})
it("should create the first application from scratch", () => {
const appName = "Cypress Tests"
cy.createApp(appName)
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)
cy.applicationInAppTable(appName)
cy.deleteApp(appName)
})
it("should generate the first application from a template", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500)
// Navigate to Create new app section if apps already exist
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
}
})
cy.get(".template-category-filters").should("exist")
cy.get(".template-categories").should("exist")
// Select template
cy.get('.template-category').eq(0).within(() => {
const card = cy.get('.template-card').eq(0).should("exist");
const cardOverlay = card.get('.template-thumbnail-action-overlay').should("exist")
cardOverlay.invoke("show")
cardOverlay.get("button").contains("Use template").should("exist").click({force: true})
})
// CMD Create app from theme card
cy.get(".spectrum-Modal").should('be.visible')
const templateName = cy.get(".spectrum-Modal .template-thumbnail-text")
templateName.invoke('text')
.then(templateNameText => {
const templateNameParsed = "/"+templateNameText.toLowerCase().replace(/\s+/g, "-")
cy.get(".spectrum-Modal input").eq(0).should("have.value", templateNameText)
cy.get(".spectrum-Modal input").eq(1).should("have.value", templateNameParsed)
cy.get(".spectrum-Modal .spectrum-ButtonGroup").contains("Create app").click()
cy.wait(5000)
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)
cy.applicationInAppTable(templateNameText)
cy.deleteApp(templateNameText)
});
})
it("should display a second application and app filtering", () => {
// Create first app
const appName = "Cypress Tests"
cy.createApp(appName)
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500)
// Create second app
const secondAppName = "Second App Demo"
cy.createApp(secondAppName)
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500)
//Both applications should exist and be searchable
cy.searchForApplication(appName)
cy.searchForApplication(secondAppName)
cy.deleteApp(secondAppName)
})
})
})

View File

@ -33,7 +33,7 @@ filterTests(['smoke', 'all'], () => {
cy.get(".spectrum-Button--cta").click()
})
cy.contains("Setup").click()
cy.get(".spectrum-Picker-label").click()
cy.get(".spectrum-Picker-label").eq(1).click()
cy.contains("dog").click()
cy.get(".spectrum-Textfield-input")
.first()

View File

@ -27,10 +27,13 @@ filterTests(["smoke", "all"], () => {
it("updates a column on the table", () => {
cy.get(".title").click()
cy.get(".spectrum-Table-editIcon > use").click()
cy.get("input").eq(1).type("updated", { force: true })
cy.get(".modal-inner-wrapper").within(() => {
cy.get("input").eq(0).type("updated", { force: true })
// Unset table display column
cy.get(".spectrum-Switch-input").eq(1).click()
cy.contains("Save Column").click()
})
cy.contains("nameupdated ").should("contain", "nameupdated")
})

View File

@ -7,6 +7,8 @@ filterTests(["smoke", "all"], () => {
})
it("should create a user", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)
cy.createUser("bbuser@test.com")
cy.get(".spectrum-Table").should("contain", "bbuser")
})
@ -21,95 +23,104 @@ filterTests(["smoke", "all"], () => {
cy.get(".spectrum-Table").eq(0).contains("No rows found")
})
it("should assign role types", () => {
// 3 apps minimum required - to assign an app to each role type
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length < 3) {
for (let i = 1; i < 3; i++) {
const uuid = () => Cypress._.random(0, 1e6)
const name = uuid()
cy.createApp(name)
if (Cypress.env("TEST_ENV")) {
it("should assign role types", () => {
// 3 apps minimum required - to assign an app to each role type
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length < 3) {
for (let i = 1; i < 3; i++) {
const uuid = () => Cypress._.random(0, 1e6)
const name = uuid()
if(i < 1){
cy.createApp(name)
} else {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500)
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
cy.createAppFromScratch(name)
}
}
}
}
})
// Navigate back to the user
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500)
cy.get(".spectrum-SideNav").contains("Users").click()
cy.wait(500)
cy.get(".spectrum-Table").contains("bbuser").click()
cy.wait(1000)
for (let i = 0; i < 3; i++) {
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.eq(0)
.find(".spectrum-Table-cell")
.eq(0)
.click()
cy.wait(500)
cy.get(".spectrum-Dialog-grid")
.contains("Choose an option")
.click()
.then(() => {
cy.wait(1000)
if (i == 0) {
cy.get(".spectrum-Popover").contains("Admin").click()
}
if (i == 1) {
cy.get(".spectrum-Popover").contains("Power").click()
}
if (i == 2) {
cy.get(".spectrum-Popover").contains("Basic").click()
}
cy.wait(1000)
cy.get(".spectrum-Button")
.contains("Update role")
.click({ force: true })
})
}
// Confirm roles exist within Configure roles table
cy.wait(2000)
cy.get(".spectrum-Table")
.eq(0)
.within(assginedRoles => {
expect(assginedRoles).to.contain("Admin")
expect(assginedRoles).to.contain("Power")
expect(assginedRoles).to.contain("Basic")
})
})
// Navigate back to the user
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500)
cy.get(".spectrum-SideNav").contains("Users").click()
cy.wait(500)
cy.get(".spectrum-Table").contains("bbuser").click()
cy.wait(1000)
for (let i = 0; i < 3; i++) {
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.eq(0)
.find(".spectrum-Table-cell")
.eq(0)
.click()
cy.wait(500)
cy.get(".spectrum-Dialog-grid")
.contains("Choose an option")
.click()
.then(() => {
cy.wait(1000)
if (i == 0) {
cy.get(".spectrum-Popover").contains("Admin").click()
}
if (i == 1) {
cy.get(".spectrum-Popover").contains("Power").click()
}
if (i == 2) {
cy.get(".spectrum-Popover").contains("Basic").click()
}
cy.wait(1000)
cy.get(".spectrum-Button")
.contains("Update role")
.click({ force: true })
})
}
// Confirm roles exist within Configure roles table
cy.wait(2000)
cy.get(".spectrum-Table")
.eq(0)
.within(assginedRoles => {
expect(assginedRoles).to.contain("Admin")
expect(assginedRoles).to.contain("Power")
expect(assginedRoles).to.contain("Basic")
})
})
it("should unassign role types", () => {
// Set each app within Configure roles table to 'No Access'
cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.its("length")
.then(len => {
for (let i = 0; i < len; i++) {
cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.eq(0)
.find(".spectrum-Table-cell")
.eq(0)
.click()
.then(() => {
cy.get(".spectrum-Picker").eq(1).click({ force: true })
cy.wait(500)
cy.get(".spectrum-Popover").contains("No Access").click()
})
cy.get(".spectrum-Button")
.contains("Update role")
.click({ force: true })
cy.wait(1000)
}
})
// Confirm Configure roles table no longer has any apps in it
cy.get(".spectrum-Table").eq(0).contains("No rows found")
})
it("should unassign role types", () => {
// Set each app within Configure roles table to 'No Access'
cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.its("length")
.then(len => {
for (let i = 0; i < len; i++) {
cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.eq(0)
.find(".spectrum-Table-cell")
.eq(0)
.click()
.then(() => {
cy.get(".spectrum-Picker").eq(1).click({ force: true })
cy.wait(500)
cy.get(".spectrum-Popover").contains("No Access").click()
})
cy.get(".spectrum-Button")
.contains("Update role")
.click({ force: true })
cy.wait(1000)
}
})
// Confirm Configure roles table no longer has any apps in it
cy.get(".spectrum-Table").eq(0).contains("No rows found")
})
}
it("should enable Developer access", () => {
// Enable Developer access

View File

@ -4,6 +4,7 @@ filterTests(['smoke', 'all'], () => {
context("Create a View", () => {
before(() => {
cy.login()
cy.createTestApp()
cy.createTable("data")
cy.addColumn("data", "group", "Text")

View File

@ -1,43 +1,47 @@
import filterTests from "../../support/filterTests"
filterTests(['smoke', 'all'], () => {
context("REST Datasource Testing", () => {
before(() => {
cy.login()
cy.createTestApp()
})
const datasource = "REST"
const restUrl = "https://api.openbrewerydb.org/breweries"
it("Should add REST data source with incorrect API", () => {
// Select REST data source
cy.selectExternalDatasource(datasource)
// Enter incorrect api & attempt to send query
cy.wait(500)
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.intercept('**/preview').as('queryError')
cy.get("input").clear().type("random text")
cy.get(".spectrum-Button").contains("Send").click({ force: true })
// Intercept Request after button click & apply assertions
cy.wait("@queryError")
cy.get("@queryError").its('response.body')
.should('have.property', 'message', 'Invalid URL: http://random text?')
cy.get("@queryError").its('response.body')
.should('have.property', 'status', 400)
})
it("should add and configure a REST datasource", () => {
// Select REST datasource and create query
cy.selectExternalDatasource(datasource)
cy.wait(500)
// createRestQuery confirms query creation
cy.createRestQuery("GET", restUrl)
// Confirm status code response within REST datasource
cy.get(".spectrum-FieldLabel")
.contains("Status")
.children()
.should('contain', 200)
})
filterTests(["smoke", "all"], () => {
context("REST Datasource Testing", () => {
before(() => {
cy.login()
cy.createTestApp()
})
const datasource = "REST"
const restUrl = "https://api.openbrewerydb.org/breweries"
it("Should add REST data source with incorrect API", () => {
// Select REST data source
cy.selectExternalDatasource(datasource)
// Enter incorrect api & attempt to send query
cy.wait(500)
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.intercept("**/preview").as("queryError")
cy.get("input").clear().type("random text")
cy.get(".spectrum-Button").contains("Send").click({ force: true })
// Intercept Request after button click & apply assertions
cy.wait("@queryError")
cy.get("@queryError")
.its("response.body")
.should("have.property", "message", "Invalid URL: http://random text?")
cy.get("@queryError")
.its("response.body")
.should("have.property", "status", 400)
})
it("should add and configure a REST datasource", () => {
// Select REST datasource and create query
cy.selectExternalDatasource(datasource)
cy.wait(500)
// createRestQuery confirms query creation
cy.createRestQuery("GET", restUrl, "/breweries")
// Confirm status code response within REST datasource
cy.wait(1000)
cy.get(".stats").within(() => {
cy.get(".spectrum-FieldLabel")
.eq(0)
.should("contain", 200)
})
})
})
})

Some files were not shown because too many files have changed in this diff Show More