Merge branch 'develop' of github.com:Budibase/budibase into cheeks-lab-day-devtools
This commit is contained in:
commit
c2d61d8ebc
|
@ -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
|
|
@ -24,9 +24,28 @@
|
||||||
{
|
{
|
||||||
"files": ["*.svelte"],
|
"files": ["*.svelte"],
|
||||||
"processor": "svelte3/svelte3"
|
"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": {
|
"rules": {
|
||||||
"no-self-assign": "off"
|
"no-self-assign": "off"
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"GeolocationPositionError": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,17 @@ jobs:
|
||||||
fi
|
fi
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
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
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -23,12 +23,24 @@ jobs:
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
aws-region: eu-west-1
|
aws-region: eu-west-1
|
||||||
|
|
||||||
|
|
||||||
- name: Get the latest budibase release version
|
- name: Get the latest budibase release version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
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
|
- name: Pull values.yaml from budibase-infra
|
||||||
run: |
|
run: |
|
||||||
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
||||||
|
|
|
@ -47,6 +47,13 @@ jobs:
|
||||||
yarn
|
yarn
|
||||||
yarn build
|
yarn build
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
- name: Build OpenAPI spec
|
||||||
|
run: |
|
||||||
|
pushd packages/server
|
||||||
|
yarn
|
||||||
|
yarn specs
|
||||||
|
popd
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@v1
|
uses: azure/setup-helm@v1
|
||||||
|
@ -56,6 +63,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
git config user.name "Budibase Helm Bot"
|
git config user.name "Budibase Helm Bot"
|
||||||
git config user.email "<>"
|
git config user.email "<>"
|
||||||
|
git reset --hard
|
||||||
git pull
|
git pull
|
||||||
helm package charts/budibase
|
helm package charts/budibase
|
||||||
git checkout gh-pages
|
git checkout gh-pages
|
||||||
|
@ -77,3 +85,5 @@ jobs:
|
||||||
packages/cli/build/cli-win.exe
|
packages/cli/build/cli-win.exe
|
||||||
packages/cli/build/cli-linux
|
packages/cli/build/cli-linux
|
||||||
packages/cli/build/cli-macos
|
packages/cli/build/cli-macos
|
||||||
|
packages/server/specs/openapi.yaml
|
||||||
|
packages/server/specs/openapi.json
|
||||||
|
|
|
@ -2,6 +2,8 @@ name: Budibase Smoke Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 5 * * *" # every day at 5AM
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
@ -23,10 +25,13 @@ jobs:
|
||||||
-o packages/builder/cypress.env.json \
|
-o packages/builder/cypress.env.json \
|
||||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/test/cypress.env.json
|
-L https://api.github.com/repos/budibase/budibase-infra/contents/test/cypress.env.json
|
||||||
wc -l packages/builder/cypress.env.json
|
wc -l packages/builder/cypress.env.json
|
||||||
- run: yarn test:e2e:ci
|
|
||||||
env:
|
- name: Cypress run
|
||||||
CI: true
|
id: cypress
|
||||||
name: Budibase CI
|
uses: cypress-io/github-action@v2
|
||||||
|
with:
|
||||||
|
install: false
|
||||||
|
command: yarn test:e2e:ci
|
||||||
|
|
||||||
# TODO: upload recordings to s3
|
# TODO: upload recordings to s3
|
||||||
# - name: Configure AWS Credentials
|
# - name: Configure AWS Credentials
|
||||||
|
@ -36,11 +41,11 @@ jobs:
|
||||||
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
# aws-region: eu-west-1
|
# aws-region: eu-west-1
|
||||||
|
|
||||||
# TODO look at cypress reporters
|
- name: Discord Webhook Action
|
||||||
# - name: Discord Webhook Action
|
uses: tsickert/discord-webhook@v4.0.0
|
||||||
# uses: tsickert/discord-webhook@v4.0.0
|
with:
|
||||||
# with:
|
webhook-url: ${{ secrets.BUDI_QA_WEBHOOK }}
|
||||||
# webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
content: "Smoke test run completed with ${{ steps.cypress.outcome }}. See results at ${{ steps.cypress.dashboardUrl }}"
|
||||||
# content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
|
embed-title: ${{ steps.cypress.outcome }}
|
||||||
# embed-title: ${{ env.RELEASE_VERSION }}
|
embed-color: ${{ steps.cypress.outcome == 'success' && '3066993' || '15548997' }}
|
||||||
|
|
||||||
|
|
|
@ -96,4 +96,6 @@ hosting/proxy/.generated-nginx.prod.conf
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
hosting/.generated*
|
||||||
packages/builder/cypress.env.json
|
packages/builder/cypress.env.json
|
||||||
|
stats.html
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
node_modules
|
node_modules
|
||||||
public
|
|
||||||
dist
|
dist
|
||||||
*.spec.js
|
*.spec.js
|
||||||
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
|
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
|
||||||
packages/server/builder
|
packages/server/builder
|
||||||
packages/server/coverage
|
packages/server/coverage
|
||||||
packages/server/client
|
packages/server/client
|
||||||
|
packages/server/src/definitions/openapi.ts
|
||||||
packages/builder/.routify
|
packages/builder/.routify
|
||||||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||||
|
|
|
@ -22,9 +22,16 @@
|
||||||
"name": "Budibase Worker",
|
"name": "Budibase Worker",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"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"
|
"cwd": "${workspaceFolder}/packages/worker"
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
|
|
43
README.md
43
README.md
|
@ -11,7 +11,7 @@
|
||||||
The low code platform you'll enjoy using
|
The low code platform you'll enjoy using
|
||||||
</h3>
|
</h3>
|
||||||
<p align="center">
|
<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>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
|
@ -40,9 +40,11 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<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>
|
<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>
|
<span> · </span>
|
||||||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
|
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
|
||||||
<span> · </span>
|
<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
|
- 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 />
|
<br /><br /><br />
|
||||||
|
|
||||||
## 🏁 Get started
|
## 🏁 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.
|
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.
|
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)
|
### [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
|
## 🎓 Learning Budibase
|
||||||
|
|
||||||
The Budibase documentation [lives here](https://docs.budibase.com).
|
The Budibase documentation [lives here](https://docs.budibase.com/docs).
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ version: 0.2.8
|
||||||
appVersion: 1.0.48
|
appVersion: 1.0.48
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: couchdb
|
- name: couchdb
|
||||||
version: 3.3.4
|
version: 3.6.1
|
||||||
repository: https://apache.github.io/couchdb-helm
|
repository: https://apache.github.io/couchdb-helm
|
||||||
condition: services.couchdb.enabled
|
condition: services.couchdb.enabled
|
||||||
- name: ingress-nginx
|
- name: ingress-nginx
|
||||||
|
|
|
@ -110,6 +110,10 @@ spec:
|
||||||
value: {{ .Values.globals.cookieDomain | quote }}
|
value: {{ .Values.globals.cookieDomain | quote }}
|
||||||
- name: HTTP_MIGRATIONS
|
- name: HTTP_MIGRATIONS
|
||||||
value: {{ .Values.globals.httpMigrations | quote }}
|
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 }}
|
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: bbapps
|
name: bbapps
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
|
@ -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
|
|
@ -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).
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
|
@ -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;
|
|
@ -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
|
@ -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.
|
|
@ -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
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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"]
|
||||||
|
}
|
|
@ -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
|
@ -12,7 +12,7 @@ All ports are BLOCKED except 22 (SSH), 80 (HTTP), 443 (HTTPS), and 10000
|
||||||
|
|
||||||
* Budibase website: http://budibase.com
|
* 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})
|
To delete this message of the day: rm -rf $(readlink -f ${0})
|
||||||
|
|
|
@ -5,7 +5,7 @@ version: "3"
|
||||||
services:
|
services:
|
||||||
minio-service:
|
minio-service:
|
||||||
container_name: budi-minio-dev
|
container_name: budi-minio-dev
|
||||||
restart: always
|
restart: on-failure
|
||||||
image: minio/minio
|
image: minio/minio
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- minio_data:/data
|
||||||
|
@ -23,7 +23,7 @@ services:
|
||||||
|
|
||||||
proxy-service:
|
proxy-service:
|
||||||
container_name: budi-nginx-dev
|
container_name: budi-nginx-dev
|
||||||
restart: always
|
restart: on-failure
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
|
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
|
||||||
|
@ -38,7 +38,7 @@ services:
|
||||||
couchdb-service:
|
couchdb-service:
|
||||||
# platform: linux/amd64
|
# platform: linux/amd64
|
||||||
container_name: budi-couchdb-dev
|
container_name: budi-couchdb-dev
|
||||||
restart: always
|
restart: on-failure
|
||||||
image: ibmcom/couchdb3
|
image: ibmcom/couchdb3
|
||||||
environment:
|
environment:
|
||||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||||
|
@ -59,7 +59,7 @@ services:
|
||||||
|
|
||||||
redis-service:
|
redis-service:
|
||||||
container_name: budi-redis-dev
|
container_name: budi-redis-dev
|
||||||
restart: always
|
restart: on-failure
|
||||||
image: redis
|
image: redis
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -4,7 +4,7 @@ version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app-service:
|
app-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
image: budibase.docker.scarf.sh/budibase/apps
|
image: budibase.docker.scarf.sh/budibase/apps
|
||||||
container_name: bbapps
|
container_name: bbapps
|
||||||
environment:
|
environment:
|
||||||
|
@ -28,7 +28,7 @@ services:
|
||||||
- redis-service
|
- redis-service
|
||||||
|
|
||||||
worker-service:
|
worker-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
image: budibase.docker.scarf.sh/budibase/worker
|
image: budibase.docker.scarf.sh/budibase/worker
|
||||||
container_name: bbworker
|
container_name: bbworker
|
||||||
environment:
|
environment:
|
||||||
|
@ -53,7 +53,7 @@ services:
|
||||||
- couch-init
|
- couch-init
|
||||||
|
|
||||||
minio-service:
|
minio-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
image: minio/minio
|
image: minio/minio
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- minio_data:/data
|
||||||
|
@ -69,7 +69,7 @@ services:
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
proxy-service:
|
proxy-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${MAIN_PORT}:10000"
|
- "${MAIN_PORT}:10000"
|
||||||
container_name: bbproxy
|
container_name: bbproxy
|
||||||
|
@ -81,7 +81,7 @@ services:
|
||||||
- couchdb-service
|
- couchdb-service
|
||||||
|
|
||||||
couchdb-service:
|
couchdb-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
image: ibmcom/couchdb3
|
image: ibmcom/couchdb3
|
||||||
environment:
|
environment:
|
||||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||||
|
@ -98,13 +98,14 @@ services:
|
||||||
command: ["sh","-c","sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;"]
|
command: ["sh","-c","sleep 10 && $${PUT_CALL}/_users && $${PUT_CALL}/_replicator; fg;"]
|
||||||
|
|
||||||
redis-service:
|
redis-service:
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
image: redis
|
image: redis
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
|
|
||||||
watchtower-service:
|
watchtower-service:
|
||||||
|
restart: always
|
||||||
image: containrrr/watchtower
|
image: containrrr/watchtower
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
@ -116,7 +117,6 @@ services:
|
||||||
labels:
|
labels:
|
||||||
- "com.centurylinklabs.watchtower.enable=false"
|
- "com.centurylinklabs.watchtower.enable=false"
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
couchdb3_data:
|
couchdb3_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
|
@ -52,9 +52,8 @@ http {
|
||||||
proxy_pass http://{{ address }}:4001;
|
proxy_pass http://{{ address }}:4001;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /app/ {
|
location /app {
|
||||||
proxy_pass http://{{ address }}:4001;
|
proxy_pass http://{{ address }}:4001;
|
||||||
rewrite ^/app/(.*)$ /$1 break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location /builder {
|
location /builder {
|
||||||
|
@ -76,6 +75,7 @@ http {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
|
||||||
proxy_connect_timeout 300;
|
proxy_connect_timeout 300;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
@ -91,4 +91,4 @@ http {
|
||||||
gzip off;
|
gzip off;
|
||||||
gzip_comp_level 4;
|
gzip_comp_level 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,18 +19,11 @@ http {
|
||||||
tcp_nodelay on;
|
tcp_nodelay on;
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
types_hash_max_size 2048;
|
types_hash_max_size 2048;
|
||||||
{{#if compose}}
|
resolver {{ resolver }} valid=10s ipv6=off;
|
||||||
resolver 127.0.0.11 ipv6=off;
|
|
||||||
{{/if}}
|
|
||||||
{{#if k8s}}
|
|
||||||
resolver kube-dns.kube-system.svc.cluster.local valid=10s;
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
|
|
||||||
# buffering
|
# buffering
|
||||||
client_body_buffer_size 1K;
|
|
||||||
client_header_buffer_size 1k;
|
client_header_buffer_size 1k;
|
||||||
client_max_body_size 10M;
|
client_max_body_size 20M;
|
||||||
ignore_invalid_headers off;
|
ignore_invalid_headers off;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
|
|
||||||
|
@ -49,13 +42,25 @@ http {
|
||||||
client_max_body_size 1000m;
|
client_max_body_size 1000m;
|
||||||
ignore_invalid_headers off;
|
ignore_invalid_headers off;
|
||||||
proxy_buffering 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
|
# Security Headers
|
||||||
add_header X-Frame-Options SAMEORIGIN always;
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
add_header X-Content-Type-Options nosniff always;
|
add_header X-Content-Type-Options nosniff always;
|
||||||
add_header X-XSS-Protection "1; mode=block" 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
|
# upstreams
|
||||||
set $apps {{ apps }};
|
set $apps {{ apps }};
|
||||||
|
@ -68,7 +73,6 @@ http {
|
||||||
|
|
||||||
location /app {
|
location /app {
|
||||||
proxy_pass http://$apps:4002;
|
proxy_pass http://$apps:4002;
|
||||||
rewrite ^/app/(.*)$ /$1 break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location = / {
|
location = / {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"]
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -39,7 +39,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<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>
|
<span> · </span>
|
||||||
<a href="https://docs.budibase.com">Dokumentation</a>
|
<a href="https://docs.budibase.com">Dokumentation</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
|
@ -109,7 +109,7 @@ $ budi hosting --start
|
||||||
4. Lege einen Admin-Benutzer an.
|
4. Lege einen Admin-Benutzer an.
|
||||||
Gib die E-Mail und das Passwort für den neuen Admin-Benutzer ein.
|
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 />
|
<br />
|
||||||
|
|
||||||
|
|
|
@ -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!
|
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®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04)
|
[![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®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.76-alpha.3",
|
"version": "1.0.105-alpha.4",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
31
package.json
31
package.json
|
@ -15,7 +15,9 @@
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup-plugin-replace": "^2.2.0",
|
"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": {
|
"scripts": {
|
||||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
"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",
|
"nuke:docker": "lerna run --parallel dev:stack:nuke",
|
||||||
"clean": "lerna clean",
|
"clean": "lerna clean",
|
||||||
"kill-port": "kill-port 4001",
|
"kill-port": "kill-port 4001",
|
||||||
"dev": "yarn run kill-port && lerna link && lerna run --parallel dev:builder --concurrency 1",
|
"kill-builder": "kill-port 3000",
|
||||||
"dev:noserver": "lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
|
"kill-server": "kill-port 4001 4002",
|
||||||
"dev:server": "lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
|
"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",
|
"test": "lerna run test",
|
||||||
"lint:eslint": "eslint packages",
|
"lint:eslint": "eslint packages",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||||
"lint:fix:eslint": "eslint --fix packages",
|
"lint:fix:eslint": "eslint --fix packages",
|
||||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\"",
|
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
||||||
"lint:fix:ts": "lerna run lint:fix",
|
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||||
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
"test:e2e": "lerna run cy:test --stream",
|
||||||
"test:e2e": "lerna run cy:test",
|
"test:e2e:ci": "lerna run cy:ci --stream",
|
||||||
"test:e2e:ci": "lerna run cy:ci",
|
"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": "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": "docker build hosting/proxy -t proxy-service",
|
||||||
"build:docker:proxy:compose": "lerna run generate:proxy:compose && npm run build:docker:proxy",
|
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
|
||||||
"build:docker:proxy:preprod": "lerna run generate:proxy:preprod && npm run build:docker:proxy",
|
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
|
||||||
"build:docker:proxy:prod": "lerna run generate:proxy:prod && 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: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: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:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
"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",
|
"build:docs": "lerna run build:docs",
|
||||||
"release:helm": "node scripts/releaseHelmChart",
|
"release:helm": "node scripts/releaseHelmChart",
|
||||||
"env:multi:enable": "lerna run env:multi:enable",
|
"env:multi:enable": "lerna run env:multi:enable",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require("./src/security/encryption")
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"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",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -32,11 +32,10 @@ const populateFromDB = async (userId, tenantId) => {
|
||||||
* @param {*} populateUser function to provide the user for re-caching. default to couch db
|
* @param {*} populateUser function to provide the user for re-caching. default to couch db
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
exports.getUser = async (
|
exports.getUser = async (userId, tenantId = null, populateUser = null) => {
|
||||||
userId,
|
if (!populateUser) {
|
||||||
tenantId = null,
|
populateUser = populateFromDB
|
||||||
populateUser = populateFromDB
|
}
|
||||||
) => {
|
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
try {
|
try {
|
||||||
tenantId = getTenantId()
|
tenantId = getTenantId()
|
||||||
|
|
|
@ -22,3 +22,18 @@ exports.getAccount = async email => {
|
||||||
|
|
||||||
return json[0]
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
const { Headers } = require("../../constants")
|
const { Headers } = require("../../constants")
|
||||||
|
const { SEPARATOR, DocumentTypes } = require("../db/constants")
|
||||||
const cls = require("./FunctionContext")
|
const cls = require("./FunctionContext")
|
||||||
const { getCouch } = require("../db")
|
const { getCouch } = require("../db")
|
||||||
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
|
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) => {
|
exports.doInAppContext = (appId, task) => {
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error("appId is required")
|
||||||
|
}
|
||||||
return cls.run(() => {
|
return cls.run(() => {
|
||||||
|
// set the app tenant id
|
||||||
|
setAppTenantId(appId)
|
||||||
|
|
||||||
// set the app ID
|
// set the app ID
|
||||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ exports.DocumentTypes = {
|
||||||
APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`,
|
APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`,
|
||||||
ROLE: "role",
|
ROLE: "role",
|
||||||
MIGRATIONS: "migrations",
|
MIGRATIONS: "migrations",
|
||||||
|
DEV_INFO: "devinfo",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.StaticDatabases = {
|
exports.StaticDatabases = {
|
||||||
|
|
|
@ -9,11 +9,7 @@ const {
|
||||||
APP_PREFIX,
|
APP_PREFIX,
|
||||||
APP_DEV,
|
APP_DEV,
|
||||||
} = require("./constants")
|
} = require("./constants")
|
||||||
const {
|
const { getTenantId, getGlobalDBName } = require("../tenancy")
|
||||||
getTenantId,
|
|
||||||
getTenantIDFromAppID,
|
|
||||||
getGlobalDBName,
|
|
||||||
} = require("../tenancy")
|
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const { getCouch } = require("./index")
|
const { getCouch } = require("./index")
|
||||||
const { getAppMetadata } = require("../cache/appMetadata")
|
const { getAppMetadata } = require("../cache/appMetadata")
|
||||||
|
@ -30,6 +26,7 @@ const UNICODE_MAX = "\ufff0"
|
||||||
|
|
||||||
exports.ViewNames = {
|
exports.ViewNames = {
|
||||||
USER_BY_EMAIL: "by_email",
|
USER_BY_EMAIL: "by_email",
|
||||||
|
BY_API_KEY: "by_api_key",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.StaticDatabases = StaticDatabases
|
exports.StaticDatabases = StaticDatabases
|
||||||
|
@ -38,7 +35,6 @@ exports.DocumentTypes = DocumentTypes
|
||||||
exports.APP_PREFIX = APP_PREFIX
|
exports.APP_PREFIX = APP_PREFIX
|
||||||
exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV
|
exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
exports.getTenantIDFromAppID = getTenantIDFromAppID
|
|
||||||
exports.isDevApp = isDevApp
|
exports.isDevApp = isDevApp
|
||||||
exports.isProdAppID = isProdAppID
|
exports.isProdAppID = isProdAppID
|
||||||
exports.isDevAppID = isDevAppID
|
exports.isDevAppID = isDevAppID
|
||||||
|
@ -67,6 +63,7 @@ function getDocParams(docType, docId = null, otherProps = {}) {
|
||||||
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
|
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
exports.getDocParams = getDocParams
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new workspace ID.
|
* 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.
|
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
|
||||||
* @param {Object} db - db instance to query
|
* @param {Object} db - db instance to query
|
||||||
|
@ -454,3 +459,4 @@ exports.generateConfigID = generateConfigID
|
||||||
exports.getConfigParams = getConfigParams
|
exports.getConfigParams = getConfigParams
|
||||||
exports.getScopedFullConfig = getScopedFullConfig
|
exports.getScopedFullConfig = getScopedFullConfig
|
||||||
exports.generateNewUsageQuotaDoc = generateNewUsageQuotaDoc
|
exports.generateNewUsageQuotaDoc = generateNewUsageQuotaDoc
|
||||||
|
exports.generateDevInfoID = generateDevInfoID
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const { DocumentTypes, ViewNames } = require("./utils")
|
const { DocumentTypes, ViewNames } = require("./utils")
|
||||||
|
const { getGlobalDB } = require("../tenancy")
|
||||||
|
|
||||||
function DesignDoc() {
|
function DesignDoc() {
|
||||||
return {
|
return {
|
||||||
|
@ -9,7 +10,8 @@ function DesignDoc() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createUserEmailView = async db => {
|
exports.createUserEmailView = async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
let designDoc
|
let designDoc
|
||||||
try {
|
try {
|
||||||
designDoc = await db.get("_design/database")
|
designDoc = await db.get("_design/database")
|
||||||
|
@ -31,3 +33,51 @@ exports.createUserEmailView = async db => {
|
||||||
}
|
}
|
||||||
await db.put(designDoc)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -4,6 +4,9 @@ const { getUser } = require("../cache/user")
|
||||||
const { getSession, updateSessionTTL } = require("../security/sessions")
|
const { getSession, updateSessionTTL } = require("../security/sessions")
|
||||||
const { buildMatcherRegex, matches } = require("./matchers")
|
const { buildMatcherRegex, matches } = require("./matchers")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
const { SEPARATOR, ViewNames, queryGlobalView } = require("../../db")
|
||||||
|
const { getGlobalDB, doInTenant } = require("../tenancy")
|
||||||
|
const { decrypt } = require("../security/encryption")
|
||||||
|
|
||||||
function finalise(
|
function finalise(
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -16,6 +19,33 @@ function finalise(
|
||||||
ctx.version = version
|
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.
|
* 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
|
* 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 apiKey = ctx.request.headers[Headers.API_KEY]
|
||||||
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
||||||
// this is an internal request, no user made it
|
// this is an internal request, no user made it
|
||||||
if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) {
|
if (!authenticated && apiKey) {
|
||||||
authenticated = true
|
const populateUser = opts.populateUser ? opts.populateUser(ctx) : null
|
||||||
internal = true
|
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) {
|
if (!user && tenantId) {
|
||||||
user = { tenantId }
|
user = { tenantId }
|
||||||
|
@ -101,6 +141,7 @@ module.exports = (
|
||||||
// allow configuring for public access
|
// allow configuring for public access
|
||||||
if ((opts && opts.publicAllowed) || publicEndpoint) {
|
if ((opts && opts.publicAllowed) || publicEndpoint) {
|
||||||
finalise(ctx, { authenticated: false, version, publicEndpoint })
|
finalise(ctx, { authenticated: false, version, publicEndpoint })
|
||||||
|
return next()
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(err.status || 403, err)
|
ctx.throw(err.status || 403, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ const { authError } = require("./passport/utils")
|
||||||
const authenticated = require("./authenticated")
|
const authenticated = require("./authenticated")
|
||||||
const auditLog = require("./auditLog")
|
const auditLog = require("./auditLog")
|
||||||
const tenancy = require("./tenancy")
|
const tenancy = require("./tenancy")
|
||||||
const appTenancy = require("./appTenancy")
|
|
||||||
const internalApi = require("./internalApi")
|
const internalApi = require("./internalApi")
|
||||||
const datasourceGoogle = require("./passport/datasource/google")
|
const datasourceGoogle = require("./passport/datasource/google")
|
||||||
const csrf = require("./csrf")
|
const csrf = require("./csrf")
|
||||||
|
@ -19,7 +18,6 @@ module.exports = {
|
||||||
authenticated,
|
authenticated,
|
||||||
auditLog,
|
auditLog,
|
||||||
tenancy,
|
tenancy,
|
||||||
appTenancy,
|
|
||||||
authError,
|
authError,
|
||||||
internalApi,
|
internalApi,
|
||||||
datasource: {
|
datasource: {
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
const google = require("../google")
|
const google = require("../google")
|
||||||
const { Cookies } = require("../../../constants")
|
const { Cookies, Configs } = require("../../../constants")
|
||||||
const { clearCookie, getCookie } = require("../../../utils")
|
const { clearCookie, getCookie } = require("../../../utils")
|
||||||
const { getDB } = require("../../../db")
|
const { getDB } = require("../../../db")
|
||||||
|
const { getScopedConfig } = require("../../../db/utils")
|
||||||
const environment = require("../../../environment")
|
const environment = require("../../../environment")
|
||||||
|
const { getGlobalDB } = require("../../../tenancy")
|
||||||
|
|
||||||
async function preAuth(passport, ctx, next) {
|
async function fetchGoogleCreds() {
|
||||||
// get the relevant config
|
// try and get the config from the tenant
|
||||||
const googleConfig = {
|
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,
|
clientID: environment.GOOGLE_CLIENT_ID,
|
||||||
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
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`
|
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
||||||
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
|
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
|
||||||
|
|
||||||
|
@ -26,10 +40,7 @@ async function preAuth(passport, ctx, next) {
|
||||||
|
|
||||||
async function postAuth(passport, ctx, next) {
|
async function postAuth(passport, ctx, next) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
const config = {
|
const config = await fetchGoogleCreds()
|
||||||
clientID: environment.GOOGLE_CLIENT_ID,
|
|
||||||
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
|
||||||
}
|
|
||||||
|
|
||||||
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
||||||
const strategy = await google.strategyFactory(
|
const strategy = await google.strategyFactory(
|
||||||
|
|
|
@ -51,7 +51,10 @@ exports.strategyFactory = async function (
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(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
|
// expose for testing
|
||||||
|
|
|
@ -78,6 +78,7 @@ exports.ObjectStore = bucket => {
|
||||||
const config = {
|
const config = {
|
||||||
s3ForcePathStyle: true,
|
s3ForcePathStyle: true,
|
||||||
signatureVersion: "v4",
|
signatureVersion: "v4",
|
||||||
|
apiVersion: "2006-03-01",
|
||||||
params: {
|
params: {
|
||||||
Bucket: sanitizeBucket(bucket),
|
Bucket: sanitizeBucket(bucket),
|
||||||
},
|
},
|
||||||
|
@ -102,17 +103,21 @@ exports.makeSureBucketExists = async (client, bucketName) => {
|
||||||
.promise()
|
.promise()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const promises = STATE.bucketCreationPromises
|
const promises = STATE.bucketCreationPromises
|
||||||
|
const doesntExist = err.statusCode === 404,
|
||||||
|
noAccess = err.statusCode === 403
|
||||||
if (promises[bucketName]) {
|
if (promises[bucketName]) {
|
||||||
await promises[bucketName]
|
await promises[bucketName]
|
||||||
} else if (err.statusCode === 404) {
|
} else if (doesntExist || noAccess) {
|
||||||
// bucket doesn't exist create it
|
if (doesntExist) {
|
||||||
promises[bucketName] = client
|
// bucket doesn't exist create it
|
||||||
.createBucket({
|
promises[bucketName] = client
|
||||||
Bucket: bucketName,
|
.createBucket({
|
||||||
})
|
Bucket: bucketName,
|
||||||
.promise()
|
})
|
||||||
await promises[bucketName]
|
.promise()
|
||||||
delete promises[bucketName]
|
await promises[bucketName]
|
||||||
|
delete promises[bucketName]
|
||||||
|
}
|
||||||
// public buckets are quite hidden in the system, make sure
|
// public buckets are quite hidden in the system, make sure
|
||||||
// no bucket is set accidentally
|
// no bucket is set accidentally
|
||||||
if (PUBLIC_BUCKETS.includes(bucketName)) {
|
if (PUBLIC_BUCKETS.includes(bucketName)) {
|
||||||
|
@ -124,7 +129,7 @@ exports.makeSureBucketExists = async (client, bucketName) => {
|
||||||
.promise()
|
.promise()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw err
|
throw new Error("Unable to write to object store bucket.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,25 @@ exports.Databases = {
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
|
|
||||||
exports.getRedisOptions = (clustered = false) => {
|
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
|
let redisProtocolUrl
|
||||||
|
|
||||||
// fully qualified redis URL
|
// fully qualified redis URL
|
||||||
if (rest.length && /rediss?/.test(host)) {
|
if (/rediss?:\/\//.test(REDIS_URL)) {
|
||||||
redisProtocolUrl = REDIS_URL
|
redisProtocolUrl = REDIS_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,13 +50,13 @@ exports.getRedisOptions = (clustered = false) => {
|
||||||
if (clustered) {
|
if (clustered) {
|
||||||
opts.redisOptions = {}
|
opts.redisOptions = {}
|
||||||
opts.redisOptions.tls = {}
|
opts.redisOptions.tls = {}
|
||||||
opts.redisOptions.password = REDIS_PASSWORD
|
opts.redisOptions.password = password
|
||||||
opts.slotsRefreshTimeout = SLOT_REFRESH_MS
|
opts.slotsRefreshTimeout = SLOT_REFRESH_MS
|
||||||
opts.dnsLookup = (address, callback) => callback(null, address)
|
opts.dnsLookup = (address, callback) => callback(null, address)
|
||||||
} else {
|
} else {
|
||||||
opts.host = host
|
opts.host = host
|
||||||
opts.port = port
|
opts.port = port
|
||||||
opts.password = REDIS_PASSWORD
|
opts.password = password
|
||||||
}
|
}
|
||||||
return { opts, host, port, redisProtocolUrl }
|
return { opts, host, port, redisProtocolUrl }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
exports.lookupApiKey = async () => {}
|
|
@ -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()
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ const PermissionLevels = {
|
||||||
|
|
||||||
// these are the global types, that govern the underlying default behaviour
|
// these are the global types, that govern the underlying default behaviour
|
||||||
const PermissionTypes = {
|
const PermissionTypes = {
|
||||||
|
APP: "app",
|
||||||
TABLE: "table",
|
TABLE: "table",
|
||||||
USER: "user",
|
USER: "user",
|
||||||
AUTOMATION: "automation",
|
AUTOMATION: "automation",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { BUILTIN_PERMISSION_IDS } = require("./permissions")
|
const { BUILTIN_PERMISSION_IDS, PermissionLevels } = require("./permissions")
|
||||||
const {
|
const {
|
||||||
generateRoleID,
|
generateRoleID,
|
||||||
getRoleParams,
|
getRoleParams,
|
||||||
|
@ -180,6 +180,20 @@ exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => {
|
||||||
return opts.idOnly ? roles.map(role => role._id) : roles
|
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.
|
* 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.
|
* @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))
|
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
|
return roles
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This retrieves the required role
|
* This retrieves the required role for a resource
|
||||||
* @param permLevel
|
* @param permLevel The level of request
|
||||||
* @param resourceId
|
* @param resourceId The resource being requested
|
||||||
* @param subResourceId
|
* @param subResourceId The sub resource being requested
|
||||||
* @return {Promise<{permissions}|Object>}
|
* @return {Promise<{permissions}|Object>} returns the permissions required to access.
|
||||||
*/
|
*/
|
||||||
exports.getRequiredResourceRole = async (
|
exports.getRequiredResourceRole = async (
|
||||||
permLevel,
|
permLevel,
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
const { getDB } = require("../db")
|
const { getDB } = require("../db")
|
||||||
const { SEPARATOR, StaticDatabases, DocumentTypes } = require("../db/constants")
|
const { SEPARATOR, StaticDatabases } = require("../db/constants")
|
||||||
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("../context")
|
const {
|
||||||
|
getTenantId,
|
||||||
|
DEFAULT_TENANT_ID,
|
||||||
|
isMultiTenant,
|
||||||
|
getTenantIDFromAppID,
|
||||||
|
} = require("../context")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
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) => {
|
exports.isUserInAppTenant = (appId, user = null) => {
|
||||||
let userTenantId
|
let userTenantId
|
||||||
if (user) {
|
if (user) {
|
||||||
|
@ -145,7 +130,7 @@ exports.isUserInAppTenant = (appId, user = null) => {
|
||||||
} else {
|
} else {
|
||||||
userTenantId = getTenantId()
|
userTenantId = getTenantId()
|
||||||
}
|
}
|
||||||
const tenantId = exports.getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
const tenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||||
return tenantId === userTenantId
|
return tenantId === userTenantId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,11 @@ const {
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
ViewNames,
|
ViewNames,
|
||||||
generateGlobalUserID,
|
generateGlobalUserID,
|
||||||
|
getAllApps,
|
||||||
} = require("./db/utils")
|
} = require("./db/utils")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const { options } = require("./middleware/passport/jwt")
|
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 { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants")
|
||||||
const {
|
const {
|
||||||
getGlobalDB,
|
getGlobalDB,
|
||||||
|
@ -20,8 +21,10 @@ const { hash } = require("./hashing")
|
||||||
const userCache = require("./cache/user")
|
const userCache = require("./cache/user")
|
||||||
const env = require("./environment")
|
const env = require("./environment")
|
||||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
||||||
|
const tenancy = require("./tenancy")
|
||||||
|
|
||||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||||
|
const PROD_APP_PREFIX = "/app/"
|
||||||
|
|
||||||
function confirmAppId(possibleAppId) {
|
function confirmAppId(possibleAppId) {
|
||||||
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
||||||
|
@ -29,16 +32,35 @@ function confirmAppId(possibleAppId) {
|
||||||
: undefined
|
: 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
|
* 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.
|
* @param {object} ctx The main request body to look through.
|
||||||
* @returns {string|undefined} If an appId was found it will be returned.
|
* @returns {string|undefined} If an appId was found it will be returned.
|
||||||
*/
|
*/
|
||||||
exports.getAppId = ctx => {
|
exports.getAppIdFromCtx = async ctx => {
|
||||||
const options = [ctx.headers[Headers.APP_ID], ctx.params.appId]
|
// look in headers
|
||||||
if (ctx.subdomains) {
|
const options = [ctx.headers[Headers.APP_ID]]
|
||||||
options.push(ctx.subdomains[1])
|
|
||||||
}
|
|
||||||
let appId
|
let appId
|
||||||
for (let option of options) {
|
for (let option of options) {
|
||||||
appId = confirmAppId(option)
|
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) {
|
if (!appId && ctx.request.body && ctx.request.body.appId) {
|
||||||
appId = confirmAppId(ctx.request.body.appId)
|
appId = confirmAppId(ctx.request.body.appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// look in the url - dev app
|
||||||
let appPath =
|
let appPath =
|
||||||
ctx.request.headers.referrer ||
|
ctx.request.headers.referrer ||
|
||||||
ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX))
|
ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX))
|
||||||
if (!appId && appPath.length !== 0) {
|
if (!appId && appPath.length) {
|
||||||
appId = confirmAppId(appPath[0])
|
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
|
return appId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,25 +169,11 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
throw "Must supply an email address to view"
|
throw "Must supply an email address to view"
|
||||||
}
|
}
|
||||||
const db = getGlobalDB()
|
|
||||||
|
|
||||||
try {
|
return queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
||||||
let users = (
|
key: email.toLowerCase(),
|
||||||
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
|
include_docs: true,
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.saveUser = async (
|
exports.saveUser = async (
|
||||||
|
|
|
@ -3338,9 +3338,9 @@ mimic-fn@^2.1.0:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
||||||
version "1.2.5"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
mixin-deep@^1.2.0:
|
mixin-deep@^1.2.0:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"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",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@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/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -57,3 +57,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Toast {
|
||||||
|
pointer-events: all;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -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>
|
|
@ -13,6 +13,7 @@
|
||||||
export let icon = undefined
|
export let icon = undefined
|
||||||
export let active = false
|
export let active = false
|
||||||
export let tooltip = undefined
|
export let tooltip = undefined
|
||||||
|
export let dataCy
|
||||||
|
|
||||||
let showTooltip = false
|
let showTooltip = false
|
||||||
</script>
|
</script>
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
class:active
|
class:active
|
||||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||||
{disabled}
|
{disabled}
|
||||||
|
data-cy={dataCy}
|
||||||
on:click|preventDefault
|
on:click|preventDefault
|
||||||
on:mouseover={() => (showTooltip = true)}
|
on:mouseover={() => (showTooltip = true)}
|
||||||
on:focus={() => (showTooltip = true)}
|
on:focus={() => (showTooltip = true)}
|
||||||
|
|
|
@ -47,7 +47,9 @@
|
||||||
<use xlink:href="#spectrum-css-icon-Dash100" />
|
<use xlink:href="#spectrum-css-icon-Dash100" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="spectrum-Checkbox-label">{text || ""}</span>
|
{#if text}
|
||||||
|
<span class="spectrum-Checkbox-label">{text}</span>
|
||||||
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -19,18 +19,33 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flatpickrId = `${uuid()}-wrapper`
|
const flatpickrId = `${uuid()}-wrapper`
|
||||||
let open = false
|
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 = {
|
$: flatpickrOptions = {
|
||||||
element: `#${flatpickrId}`,
|
element: `#${flatpickrId}`,
|
||||||
enableTime: isTimeOnly || enableTime || false,
|
enableTime: timeOnly || enableTime || false,
|
||||||
noCalendar: isTimeOnly || false,
|
noCalendar: timeOnly || false,
|
||||||
altInput: true,
|
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,
|
wrap: true,
|
||||||
appendTo,
|
appendTo,
|
||||||
disableMobile: "true",
|
disableMobile: "true",
|
||||||
|
onReady: () => {
|
||||||
|
let timestamp = resolveTimeStamp(value)
|
||||||
|
if (timeOnly && timestamp) {
|
||||||
|
dispatch("change", timestamp.toISOString())
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = event => {
|
const handleChange = event => {
|
||||||
|
@ -39,10 +54,9 @@
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
newValue = newValue.toISOString()
|
newValue = newValue.toISOString()
|
||||||
}
|
}
|
||||||
// if time only set date component to today
|
// if time only set date component to 2000-01-01
|
||||||
if (timeOnly) {
|
if (timeOnly) {
|
||||||
const todayDate = new Date().toISOString().split("T")[0]
|
newValue = `2000-01-01T${newValue.split("T")[1]}`
|
||||||
newValue = `${todayDate}T${newValue.split("T")[1]}`
|
|
||||||
}
|
}
|
||||||
dispatch("change", newValue)
|
dispatch("change", newValue)
|
||||||
}
|
}
|
||||||
|
@ -76,10 +90,13 @@
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let date
|
let date
|
||||||
let time = new Date(`0-${val}`)
|
let time
|
||||||
|
|
||||||
// it is a string like 00:00:00, just time
|
// it is a string like 00:00:00, just time
|
||||||
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
|
let ts = resolveTimeStamp(val)
|
||||||
date = time
|
|
||||||
|
if (timeOnly && ts) {
|
||||||
|
date = ts
|
||||||
} else if (val instanceof Date) {
|
} else if (val instanceof Date) {
|
||||||
// Use real date obj if already parsed
|
// Use real date obj if already parsed
|
||||||
date = val
|
date = val
|
||||||
|
@ -101,7 +118,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key isTimeOnly}
|
{#key timeOnly}
|
||||||
<Flatpickr
|
<Flatpickr
|
||||||
bind:flatpickr
|
bind:flatpickr
|
||||||
value={parseDate(value)}
|
value={parseDate(value)}
|
||||||
|
|
|
@ -54,34 +54,43 @@
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKey} />
|
<svelte:window on:keydown={handleKey} />
|
||||||
|
|
||||||
<!-- These svelte if statements need to be defined like this. -->
|
{#if inline}
|
||||||
<!-- The modal transitions do not work if nested inside more than one "if" -->
|
{#if visible}
|
||||||
{#if visible && inline}
|
<div use:focusFirstInput class="spectrum-Modal inline is-open">
|
||||||
<div use:focusFirstInput class="spectrum-Modal inline is-open">
|
<slot />
|
||||||
<slot />
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{:else if visible}
|
{: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">
|
<Portal target=".modal-container">
|
||||||
<div
|
{#if visible}
|
||||||
class="spectrum-Underlay is-open"
|
<div
|
||||||
in:fade={{ duration: 200 }}
|
class="spectrum-Underlay is-open"
|
||||||
out:fade|local={{ duration: 200 }}
|
in:fade={{ duration: 200 }}
|
||||||
on:mousedown|self={cancel}
|
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}>
|
<div class="modal-wrapper" on:mousedown|self={cancel}>
|
||||||
<slot name="outside" />
|
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
|
||||||
<div
|
<slot name="outside" />
|
||||||
use:focusFirstInput
|
<div
|
||||||
class="spectrum-Modal is-open"
|
use:focusFirstInput
|
||||||
in:fly={{ y: 30, duration: 200 }}
|
class="spectrum-Modal is-open"
|
||||||
out:fly|local={{ y: 30, duration: 200 }}
|
in:fly={{ y: 30, duration: 200 }}
|
||||||
>
|
out:fly|local={{ y: 30, duration: 200 }}
|
||||||
<slot />
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</Portal>
|
</Portal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import Divider from "../Divider/Divider.svelte"
|
import Divider from "../Divider/Divider.svelte"
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
import Context from "../context"
|
import Context from "../context"
|
||||||
|
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
||||||
|
|
||||||
export let title = undefined
|
export let title = undefined
|
||||||
export let size = "S"
|
export let size = "S"
|
||||||
|
@ -102,15 +103,22 @@
|
||||||
<Button group secondary on:click={close}>{cancelText}</Button>
|
<Button group secondary on:click={close}>{cancelText}</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showConfirmButton}
|
{#if showConfirmButton}
|
||||||
<Button
|
<span class="confirm-wrap">
|
||||||
group
|
<Button
|
||||||
cta
|
group
|
||||||
{...$$restProps}
|
cta
|
||||||
disabled={confirmDisabled}
|
{...$$restProps}
|
||||||
on:click={confirm}
|
disabled={confirmDisabled}
|
||||||
>
|
on:click={confirm}
|
||||||
{confirmText}
|
>
|
||||||
</Button>
|
{#if loading}
|
||||||
|
<ProgressCircle overBackground={true} size="S" />
|
||||||
|
{/if}
|
||||||
|
{#if !loading}
|
||||||
|
{confirmText}
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -165,4 +173,12 @@
|
||||||
.secondary-action {
|
.secondary-action {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spectrum-Dialog-buttonGroup {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-wrap :global(.spectrum-Button-label) {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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()
|
|
@ -60,7 +60,7 @@ export const createNotificationStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function id() {
|
function id() {
|
||||||
return "_" + Math.random().toString(36).substr(2, 9)
|
return "_" + Math.random().toString(36).slice(2, 9)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const notifications = createNotificationStore()
|
export const notifications = createNotificationStore()
|
||||||
|
|
|
@ -8,9 +8,21 @@
|
||||||
export let allowEditRows = false
|
export let allowEditRows = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if allowSelectRows}
|
<div>
|
||||||
<Checkbox value={selected} />
|
{#if allowSelectRows}
|
||||||
{/if}
|
<Checkbox value={selected} />
|
||||||
{#if allowEditRows}
|
{/if}
|
||||||
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton>
|
{#if allowEditRows}
|
||||||
{/if}
|
<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>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
||||||
import { cloneDeep, deepGet } from "../helpers"
|
import { cloneDeep, deepGet } from "../helpers"
|
||||||
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
||||||
|
import Checkbox from "../Form/Checkbox.svelte"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The expected schema is our normal couch schemas for our tables.
|
* The expected schema is our normal couch schemas for our tables.
|
||||||
|
@ -31,7 +32,6 @@
|
||||||
export let allowEditRows = true
|
export let allowEditRows = true
|
||||||
export let allowEditColumns = true
|
export let allowEditColumns = true
|
||||||
export let selectedRows = []
|
export let selectedRows = []
|
||||||
export let editColumnTitle = "Edit"
|
|
||||||
export let customRenderers = []
|
export let customRenderers = []
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
export let autoSortColumns = true
|
export let autoSortColumns = true
|
||||||
|
@ -50,10 +50,13 @@
|
||||||
// Table state
|
// Table state
|
||||||
let height = 0
|
let height = 0
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let checkboxStatus = false
|
||||||
|
|
||||||
$: schema = fixSchema(schema)
|
$: schema = fixSchema(schema)
|
||||||
$: if (!loading) loaded = true
|
$: if (!loading) loaded = true
|
||||||
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
||||||
$: rows = fields?.length ? data || [] : []
|
$: rows = fields?.length ? data || [] : []
|
||||||
|
$: totalRowCount = rows?.length || 0
|
||||||
$: visibleRowCount = getVisibleRowCount(
|
$: visibleRowCount = getVisibleRowCount(
|
||||||
loaded,
|
loaded,
|
||||||
height,
|
height,
|
||||||
|
@ -61,12 +64,27 @@
|
||||||
rowCount,
|
rowCount,
|
||||||
rowHeight
|
rowHeight
|
||||||
)
|
)
|
||||||
$: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
|
$: heightStyle = getHeightStyle(
|
||||||
|
visibleRowCount,
|
||||||
|
rowCount,
|
||||||
|
totalRowCount,
|
||||||
|
rowHeight
|
||||||
|
)
|
||||||
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
||||||
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
|
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
|
||||||
$: showEditColumn = allowEditRows || allowSelectRows
|
$: showEditColumn = allowEditRows || allowSelectRows
|
||||||
$: cellStyles = computeCellStyles(schema)
|
$: 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 => {
|
const fixSchema = schema => {
|
||||||
let fixedSchema = {}
|
let fixedSchema = {}
|
||||||
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
||||||
|
@ -95,11 +113,16 @@
|
||||||
return Math.min(allRows, Math.ceil(height / rowHeight))
|
return Math.min(allRows, Math.ceil(height / rowHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContentStyle = (visibleRows, rowCount, rowHeight) => {
|
const getHeightStyle = (
|
||||||
if (!rowCount || !visibleRows) {
|
visibleRowCount,
|
||||||
|
rowCount,
|
||||||
|
totalRowCount,
|
||||||
|
rowHeight
|
||||||
|
) => {
|
||||||
|
if (!rowCount || !visibleRowCount || totalRowCount <= rowCount) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return `height: ${headerHeight + visibleRows * rowHeight}px;`
|
return `height: ${headerHeight + visibleRowCount * rowHeight}px;`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGridStyle = (fields, schema, showEditColumn) => {
|
const getGridStyle = (fields, schema, showEditColumn) => {
|
||||||
|
@ -197,13 +220,32 @@
|
||||||
if (!allowSelectRows) {
|
if (!allowSelectRows) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (selectedRows.includes(row)) {
|
if (selectedRows.some(selectedRow => selectedRow._id === row._id)) {
|
||||||
selectedRows = selectedRows.filter(selectedRow => selectedRow !== row)
|
selectedRows = selectedRows.filter(
|
||||||
|
selectedRow => selectedRow._id !== row._id
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
selectedRows = [...selectedRows, row]
|
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 => {
|
const computeCellStyles = schema => {
|
||||||
let styles = {}
|
let styles = {}
|
||||||
Object.keys(schema || {}).forEach(field => {
|
Object.keys(schema || {}).forEach(field => {
|
||||||
|
@ -233,18 +275,25 @@
|
||||||
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
||||||
>
|
>
|
||||||
{#if !loaded}
|
{#if !loaded}
|
||||||
<div class="loading" style={contentStyle}>
|
<div class="loading" style={heightStyle}>
|
||||||
<ProgressCircle />
|
<ProgressCircle />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="spectrum-Table" style={`${contentStyle}${gridStyle}`}>
|
<div class="spectrum-Table" style={`${heightStyle}${gridStyle}`}>
|
||||||
{#if fields.length}
|
{#if fields.length}
|
||||||
<div class="spectrum-Table-head">
|
<div class="spectrum-Table-head">
|
||||||
{#if showEditColumn}
|
{#if showEditColumn}
|
||||||
<div
|
<div
|
||||||
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
|
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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
|
@ -302,11 +351,16 @@
|
||||||
{#if showEditColumn}
|
{#if showEditColumn}
|
||||||
<div
|
<div
|
||||||
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
||||||
|
on:click={e => {
|
||||||
|
toggleSelectRow(row)
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectEditRenderer
|
<SelectEditRenderer
|
||||||
data={row}
|
data={row}
|
||||||
selected={selectedRows.includes(row)}
|
selected={selectedRows.findIndex(
|
||||||
onToggleSelection={() => toggleSelectRow(row)}
|
selectedRow => selectedRow._id === row._id
|
||||||
|
) !== -1}
|
||||||
onEdit={e => editRow(e, row)}
|
onEdit={e => editRow(e, row)}
|
||||||
{allowSelectRows}
|
{allowSelectRows}
|
||||||
{allowEditRows}
|
{allowEditRows}
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
function id() {
|
function id() {
|
||||||
return "_" + Math.random().toString(36).substr(2, 9)
|
return "_" + Math.random().toString(36).slice(2, 9)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,7 @@ export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
||||||
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||||
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
||||||
export { default as Banner } from "./Banner/Banner.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 MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
||||||
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
||||||
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
||||||
|
@ -84,6 +85,7 @@ export { default as clickOutside } from "./Actions/click_outside"
|
||||||
|
|
||||||
// Stores
|
// Stores
|
||||||
export { notifications, createNotificationStore } from "./Stores/notifications"
|
export { notifications, createNotificationStore } from "./Stores/notifications"
|
||||||
|
export { banner } from "./Stores/banner"
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
export * as Helpers from "./helpers"
|
export * as Helpers from "./helpers"
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
chalk "^2.0.0"
|
chalk "^2.0.0"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@budibase/handlebars-helpers@^0.11.7":
|
"@budibase/handlebars-helpers@^0.11.8":
|
||||||
version "0.11.8"
|
version "0.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.8.tgz#6953d29673a8c5c407e096c0a84890465c7ce841"
|
resolved "https://registry.yarnpkg.com/@budibase/handlebars-helpers/-/handlebars-helpers-0.11.8.tgz#6953d29673a8c5c407e096c0a84890465c7ce841"
|
||||||
integrity sha512-ggWJUt0GqsHFAEup5tlWlcrmYML57nKhpNGGLzVsqXVYN8eVmf3xluYmmMe7fDYhQH0leSprrdEXmsdFQF3HAQ==
|
integrity sha512-ggWJUt0GqsHFAEup5tlWlcrmYML57nKhpNGGLzVsqXVYN8eVmf3xluYmmMe7fDYhQH0leSprrdEXmsdFQF3HAQ==
|
||||||
|
@ -53,12 +53,12 @@
|
||||||
to-gfm-code-block "^0.1.1"
|
to-gfm-code-block "^0.1.1"
|
||||||
year "^0.2.1"
|
year "^0.2.1"
|
||||||
|
|
||||||
"@budibase/string-templates@^1.0.72-alpha.0":
|
"@budibase/string-templates@^1.0.105-alpha.4":
|
||||||
version "1.0.75"
|
version "1.0.108"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.75.tgz#5b4061f1a626160ec092f32f036541376298100c"
|
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.108.tgz#14949560148ef11b6b385952ed5787c12b890f2c"
|
||||||
integrity sha512-hPgr6n5cpSCGFEha5DS/P+rtRXOLc72M6y4J/scl59JvUi/ZUJkjRgJdpQPdBLu04CNKp89V59+rAqAuDjOC0g==
|
integrity sha512-7Tts91Dzy+A7OdObTIaBNAdaixC7wmabnNTWYqk1d6TM6H5yv++bd/a9gVdUM7ptsuMy7uoP/ZoTZZRhp3ozfA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@budibase/handlebars-helpers" "^0.11.7"
|
"@budibase/handlebars-helpers" "^0.11.8"
|
||||||
dayjs "^1.10.4"
|
dayjs "^1.10.4"
|
||||||
handlebars "^4.7.6"
|
handlebars "^4.7.6"
|
||||||
handlebars-utils "^1.0.6"
|
handlebars-utils "^1.0.6"
|
||||||
|
@ -2177,9 +2177,9 @@ minimatch@^3.0.4:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimist@^1.2.0, minimist@^1.2.5:
|
minimist@^1.2.0, minimist@^1.2.5:
|
||||||
version "1.2.5"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
mixin-deep@^1.2.0:
|
mixin-deep@^1.2.0:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
|
@ -3410,9 +3410,9 @@ typo-js@*:
|
||||||
integrity sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg==
|
integrity sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg==
|
||||||
|
|
||||||
uglify-js@^3.1.4:
|
uglify-js@^3.1.4:
|
||||||
version "3.15.1"
|
version "3.15.3"
|
||||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.1.tgz#9403dc6fa5695a6172a91bc983ea39f0f7c9086d"
|
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.3.tgz#9aa82ca22419ba4c0137642ba0df800cb06e0471"
|
||||||
integrity sha512-FAGKF12fWdkpvNJZENacOH0e/83eG6JyVQyanIJaBXCN1J11TUQv1T1/z8S+Z0CG0ZPk1nPcreF/c7lrTd0TEQ==
|
integrity sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg==
|
||||||
|
|
||||||
unbox-primitive@^1.0.0:
|
unbox-primitive@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
|
@ -3503,9 +3503,9 @@ vendors@^1.0.0:
|
||||||
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
|
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
|
||||||
|
|
||||||
vm2@^3.9.4:
|
vm2@^3.9.4:
|
||||||
version "3.9.8"
|
version "3.9.9"
|
||||||
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.8.tgz#e99c000db042735cd2f94d8db6c42163a17be04e"
|
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.9.tgz#c0507bc5fbb99388fad837d228badaaeb499ddc5"
|
||||||
integrity sha512-/1PYg/BwdKzMPo8maOZ0heT7DLI0DAFTm7YQaz/Lim9oIaFZsJs3EdtalvXuBfZwczNwsYhju75NW4d6E+4q+w==
|
integrity sha512-xwTm7NLh/uOjARRBs8/95H0e8fT3Ukw5D/JJWhxMbhKzNh1Nu981jQKvkep9iKYNxzlVrdzD0mlBGkDKZWprlw==
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^8.7.0"
|
acorn "^8.7.0"
|
||||||
acorn-walk "^8.2.0"
|
acorn-walk "^8.2.0"
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
{
|
{
|
||||||
"baseUrl": "http://localhost:10001",
|
"baseUrl": "http://localhost:4100",
|
||||||
"video": false,
|
"video": false,
|
||||||
"projectId": "bmbemn",
|
"projectId": "bmbemn",
|
||||||
"env": {
|
"env": {
|
||||||
"PORT": "10001",
|
"PORT": "4100",
|
||||||
|
"WORKER_PORT": "4200",
|
||||||
"JWT_SECRET": "test",
|
"JWT_SECRET": "test",
|
||||||
"HOST_IP": ""
|
"HOST_IP": ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ filterTests(['all'], () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should change the icon and colour for an application", () => {
|
it("should change the icon and colour for an application", () => {
|
||||||
// Search for test application
|
// Search for test application
|
||||||
cy.searchForApplication("Cypress Tests")
|
cy.applicationInAppTable("Cypress Tests")
|
||||||
cy.get(".appTable")
|
cy.get(".appTable")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get(".spectrum-Icon").eq(1).click()
|
||||||
|
|
|
@ -2,11 +2,164 @@ import filterTests from '../support/filterTests'
|
||||||
|
|
||||||
filterTests(['smoke', 'all'], () => {
|
filterTests(['smoke', 'all'], () => {
|
||||||
context("Create an Application", () => {
|
context("Create an Application", () => {
|
||||||
it("should create a new application", () => {
|
|
||||||
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.createTestApp()
|
cy.deleteApp("Cypress Tests")
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
|
||||||
cy.contains("Cypress Tests").should("exist")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -33,7 +33,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.get(".spectrum-Button--cta").click()
|
cy.get(".spectrum-Button--cta").click()
|
||||||
})
|
})
|
||||||
cy.contains("Setup").click()
|
cy.contains("Setup").click()
|
||||||
cy.get(".spectrum-Picker-label").click()
|
cy.get(".spectrum-Picker-label").eq(1).click()
|
||||||
cy.contains("dog").click()
|
cy.contains("dog").click()
|
||||||
cy.get(".spectrum-Textfield-input")
|
cy.get(".spectrum-Textfield-input")
|
||||||
.first()
|
.first()
|
||||||
|
|
|
@ -27,10 +27,13 @@ filterTests(["smoke", "all"], () => {
|
||||||
it("updates a column on the table", () => {
|
it("updates a column on the table", () => {
|
||||||
cy.get(".title").click()
|
cy.get(".title").click()
|
||||||
cy.get(".spectrum-Table-editIcon > use").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
|
// Unset table display column
|
||||||
cy.get(".spectrum-Switch-input").eq(1).click()
|
cy.get(".spectrum-Switch-input").eq(1).click()
|
||||||
cy.contains("Save Column").click()
|
cy.contains("Save Column").click()
|
||||||
|
})
|
||||||
cy.contains("nameupdated ").should("contain", "nameupdated")
|
cy.contains("nameupdated ").should("contain", "nameupdated")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ filterTests(["smoke", "all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should create a user", () => {
|
it("should create a user", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
cy.createUser("bbuser@test.com")
|
cy.createUser("bbuser@test.com")
|
||||||
cy.get(".spectrum-Table").should("contain", "bbuser")
|
cy.get(".spectrum-Table").should("contain", "bbuser")
|
||||||
})
|
})
|
||||||
|
@ -21,95 +23,104 @@ filterTests(["smoke", "all"], () => {
|
||||||
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should assign role types", () => {
|
if (Cypress.env("TEST_ENV")) {
|
||||||
// 3 apps minimum required - to assign an app to each role type
|
it("should assign role types", () => {
|
||||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
// 3 apps minimum required - to assign an app to each role type
|
||||||
.its("body")
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
.then(val => {
|
.its("body")
|
||||||
if (val.length < 3) {
|
.then(val => {
|
||||||
for (let i = 1; i < 3; i++) {
|
if (val.length < 3) {
|
||||||
const uuid = () => Cypress._.random(0, 1e6)
|
for (let i = 1; i < 3; i++) {
|
||||||
const name = uuid()
|
const uuid = () => Cypress._.random(0, 1e6)
|
||||||
cy.createApp(name)
|
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 })
|
|
||||||
})
|
})
|
||||||
}
|
// Navigate back to the user
|
||||||
// Confirm roles exist within Configure roles table
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(2000)
|
cy.wait(500)
|
||||||
cy.get(".spectrum-Table")
|
cy.get(".spectrum-SideNav").contains("Users").click()
|
||||||
.eq(0)
|
cy.wait(500)
|
||||||
.within(assginedRoles => {
|
cy.get(".spectrum-Table").contains("bbuser").click()
|
||||||
expect(assginedRoles).to.contain("Admin")
|
cy.wait(1000)
|
||||||
expect(assginedRoles).to.contain("Power")
|
for (let i = 0; i < 3; i++) {
|
||||||
expect(assginedRoles).to.contain("Basic")
|
cy.get(".spectrum-Table")
|
||||||
})
|
.eq(1)
|
||||||
})
|
.find(".spectrum-Table-row")
|
||||||
|
.eq(0)
|
||||||
it("should unassign role types", () => {
|
.find(".spectrum-Table-cell")
|
||||||
// Set each app within Configure roles table to 'No Access'
|
.eq(0)
|
||||||
cy.get(".spectrum-Table")
|
.click()
|
||||||
.eq(0)
|
cy.wait(500)
|
||||||
.find(".spectrum-Table-row")
|
cy.get(".spectrum-Dialog-grid")
|
||||||
.its("length")
|
.contains("Choose an option")
|
||||||
.then(len => {
|
.click()
|
||||||
for (let i = 0; i < len; i++) {
|
.then(() => {
|
||||||
cy.get(".spectrum-Table")
|
cy.wait(1000)
|
||||||
.eq(0)
|
if (i == 0) {
|
||||||
.find(".spectrum-Table-row")
|
cy.get(".spectrum-Popover").contains("Admin").click()
|
||||||
.eq(0)
|
}
|
||||||
.find(".spectrum-Table-cell")
|
if (i == 1) {
|
||||||
.eq(0)
|
cy.get(".spectrum-Popover").contains("Power").click()
|
||||||
.click()
|
}
|
||||||
.then(() => {
|
if (i == 2) {
|
||||||
cy.get(".spectrum-Picker").eq(1).click({ force: true })
|
cy.get(".spectrum-Popover").contains("Basic").click()
|
||||||
cy.wait(500)
|
}
|
||||||
cy.get(".spectrum-Popover").contains("No Access").click()
|
cy.wait(1000)
|
||||||
})
|
cy.get(".spectrum-Button")
|
||||||
cy.get(".spectrum-Button")
|
.contains("Update role")
|
||||||
.contains("Update role")
|
.click({ force: true })
|
||||||
.click({ force: true })
|
})
|
||||||
cy.wait(1000)
|
}
|
||||||
}
|
// Confirm roles exist within Configure roles table
|
||||||
})
|
cy.wait(2000)
|
||||||
// Confirm Configure roles table no longer has any apps in it
|
cy.get(".spectrum-Table")
|
||||||
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
.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 enable Developer access", () => {
|
it("should enable Developer access", () => {
|
||||||
// Enable Developer access
|
// Enable Developer access
|
||||||
|
|
|
@ -4,6 +4,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
context("Create a View", () => {
|
context("Create a View", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
|
|
||||||
cy.createTestApp()
|
cy.createTestApp()
|
||||||
cy.createTable("data")
|
cy.createTable("data")
|
||||||
cy.addColumn("data", "group", "Text")
|
cy.addColumn("data", "group", "Text")
|
||||||
|
|
|
@ -1,43 +1,47 @@
|
||||||
import filterTests from "../../support/filterTests"
|
import filterTests from "../../support/filterTests"
|
||||||
|
|
||||||
filterTests(['smoke', 'all'], () => {
|
filterTests(["smoke", "all"], () => {
|
||||||
context("REST Datasource Testing", () => {
|
context("REST Datasource Testing", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.createTestApp()
|
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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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
Loading…
Reference in New Issue