forked from tsai/budibase
192 changed files with 4605 additions and 1734 deletions
@ -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 |
||||
@ -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 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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 |
||||
@ -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) |
|
||||
}) |
|
||||
} |
|
||||
@ -1,133 +1,128 @@ |
|||||
import filterTests from "../support/filterTests" |
import filterTests from "../support/filterTests" |
||||
|
|
||||
filterTests(['all'], () => { |
filterTests(['all'], () => { |
||||
context("Rename an App", () => { |
context("Rename an App", () => { |
||||
beforeEach(() => { |
beforeEach(() => { |
||||
cy.login() |
cy.login() |
||||
cy.createTestApp() |
cy.createTestApp() |
||||
}) |
}) |
||||
|
|
||||
it("should rename an unpublished application", () => { |
it("should rename an unpublished application", () => { |
||||
const appName = "Cypress Tests" |
const appName = "Cypress Tests" |
||||
const appRename = "Cypress Renamed" |
const appRename = "Cypress Renamed" |
||||
// Rename app, Search for app, Confirm name was changed
|
// Rename app, Search for app, Confirm name was changed
|
||||
cy.get(".home-logo").click() |
cy.visit(`${Cypress.config().baseUrl}/builder`) |
||||
renameApp(appName, appRename) |
cy.wait(500) |
||||
cy.reload() |
renameApp(appName, appRename) |
||||
cy.wait(1000) |
cy.reload() |
||||
cy.searchForApplication(appRename) |
cy.wait(1000) |
||||
cy.get(".appTable").find(".title").should("have.length", 1) |
cy.searchForApplication(appRename) |
||||
// Set app name back to Cypress Tests
|
cy.get(".appTable").find(".title").should("have.length", 1) |
||||
cy.reload() |
cy.applicationInAppTable(appRename) |
||||
cy.wait(1000) |
// Set app name back to Cypress Tests
|
||||
renameApp(appRename, appName) |
cy.reload() |
||||
|
cy.wait(1000) |
||||
|
renameApp(appRename, appName) |
||||
}) |
}) |
||||
|
|
||||
xit("Should rename a published application", () => { |
xit("Should rename a published application", () => { |
||||
// It is not possible to rename a published application
|
// It is not possible to rename a published application
|
||||
const appName = "Cypress Tests" |
const appName = "Cypress Tests" |
||||
const appRename = "Cypress Renamed" |
const appRename = "Cypress Renamed" |
||||
// Publish the app
|
// Publish the app
|
||||
cy.get(".toprightnav") |
cy.get(".toprightnav") |
||||
cy.get(".spectrum-Button").contains("Publish").click({force: true}) |
cy.get(".spectrum-Button").contains("Publish").click({ force: true }) |
||||
cy.get(".spectrum-Dialog-grid") |
cy.get(".spectrum-Dialog-grid") |
||||
.within(() => { |
.within(() => { |
||||
// Click publish again within the modal
|
// Click publish again within the modal
|
||||
cy.get(".spectrum-Button").contains("Publish").click({force: true}) |
cy.get(".spectrum-Button").contains("Publish").click({ force: true }) |
||||
}) |
}) |
||||
// Rename app, Search for app, Confirm name was changed
|
// Rename app, Search for app, Confirm name was changed
|
||||
cy.get(".home-logo").click() |
cy.visit(`${Cypress.config().baseUrl}/builder`) |
||||
renameApp(appName, appRename, true) |
cy.wait(500) |
||||
cy.searchForApplication(appRename) |
renameApp(appName, appRename, true) |
||||
cy.get(".appTable").find(".wrapper").should("have.length", 1) |
cy.get(".appTable").find(".wrapper").should("have.length", 1) |
||||
|
cy.applicationInAppTable(appRename) |
||||
}) |
}) |
||||
|
|
||||
it("Should try to rename an application to have no name", () => { |
it("Should try to rename an application to have no name", () => { |
||||
const appName = "Cypress Tests" |
const appName = "Cypress Tests" |
||||
cy.get(".home-logo").click() |
cy.visit(`${Cypress.config().baseUrl}/builder`) |
||||
renameApp(appName, " ", false, true) |
cy.wait(500) |
||||
cy.wait(500) |
renameApp(appName, " ", false, true) |
||||
// Close modal and confirm name has not been changed
|
cy.wait(500) |
||||
cy.get(".spectrum-Dialog-grid").contains("Cancel").click() |
// Close modal and confirm name has not been changed
|
||||
cy.reload() |
cy.get(".spectrum-Dialog-grid").contains("Cancel").click() |
||||
cy.wait(1000) |
cy.reload() |
||||
cy.searchForApplication(appName) |
cy.wait(1000) |
||||
cy.get(".appTable").find(".title").should("have.length", 1) |
cy.applicationInAppTable(appName) |
||||
|
|
||||
}) |
}) |
||||
|
|
||||
xit("Should create two applications with the same name", () => { |
xit("Should create two applications with the same name", () => { |
||||
// It is not possible to have applications with the same name
|
// It is not possible to have applications with the same name
|
||||
const appName = "Cypress Tests" |
const appName = "Cypress Tests" |
||||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
cy.visit(`${Cypress.config().baseUrl}/builder`) |
||||
cy.wait(500) |
cy.wait(500) |
||||
cy.get(".spectrum-Button").contains("Create app").click({force: true}) |
cy.get(".spectrum-Button").contains("Create app").click({ force: true }) |
||||
cy.contains(/Start from scratch/).click() |
cy.contains(/Start from scratch/).click() |
||||
cy.get(".spectrum-Modal") |
cy.get(".spectrum-Modal") |
||||
.within(() => { |
.within(() => { |
||||
cy.get("input").eq(0).type(appName) |
cy.get("input").eq(0).type(appName) |
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true}) |
cy.get(".spectrum-ButtonGroup").contains("Create app").click({ force: true }) |
||||
cy.get(".error").should("have.text", "Another app with the same name already exists") |
cy.get(".error").should("have.text", "Another app with the same name already exists") |
||||
}) |
}) |
||||
}) |
}) |
||||
|
|
||||
it("should validate application names", () => { |
it("should validate application names", () => { |
||||
// App name must be letters, numbers and spaces only
|
// App name must be letters, numbers and spaces only
|
||||
// This test checks numbers and special characters specifically
|
// This test checks numbers and special characters specifically
|
||||
const appName = "Cypress Tests" |
const appName = "Cypress Tests" |
||||
const numberName = 12345 |
const numberName = 12345 |
||||
const specialCharName = "£$%^" |
const specialCharName = "£$%^" |
||||
cy.get(".home-logo").click() |
cy.visit(`${Cypress.config().baseUrl}/builder`) |
||||
renameApp(appName, numberName) |
cy.wait(500) |
||||
cy.reload() |
renameApp(appName, numberName) |
||||
cy.wait(1000) |
cy.reload() |
||||
cy.searchForApplication(numberName) |
cy.wait(1000) |
||||
cy.get(".appTable").find(".title").should("have.length", 1) |
cy.applicationInAppTable(numberName) |
||||
cy.reload() |
cy.reload() |
||||
cy.wait(1000) |
cy.wait(1000) |
||||
renameApp(numberName, specialCharName) |
renameApp(numberName, specialCharName) |
||||
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") |
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") |
||||
// Set app name back to Cypress Tests
|
// Set app name back to Cypress Tests
|
||||
cy.reload() |
cy.reload() |
||||
cy.wait(1000) |
cy.wait(1000) |
||||
renameApp(numberName, appName) |
renameApp(numberName, appName) |
||||
}) |
}) |
||||
|
|
||||
const renameApp = (originalName, changedName, published, noName) => { |
|
||||
cy.searchForApplication(originalName) |
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) |
|
||||
.its("body") |
|
||||
.then(val => { |
|
||||
if (val.length > 0) { |
|
||||
cy.get(".appTable") |
|
||||
.within(() => { |
|
||||
cy.get(".spectrum-Icon").eq(1).click() |
|
||||
}) |
|
||||
// Check for when an app is published
|
|
||||
if (published == true){ |
|
||||
// Should not have Edit as option, will unpublish app
|
|
||||
cy.should("not.have.value", "Edit") |
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click() |
|
||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() |
|
||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click() |
|
||||
} |
|
||||
cy.contains("Edit").click() |
|
||||
cy.get(".spectrum-Modal") |
|
||||
.within(() => { |
|
||||
if (noName == true){ |
|
||||
cy.get("input").clear() |
|
||||
cy.get(".spectrum-Dialog-grid").click() |
|
||||
.contains("App name must be letters, numbers and spaces only") |
|
||||
return cy |
|
||||
} |
|
||||
cy.get("input").clear() |
|
||||
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur() |
|
||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true}) |
|
||||
cy.wait(500) |
|
||||
}) |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
} |
const renameApp = (originalName, changedName, published, noName) => { |
||||
|
cy.searchForApplication(originalName) |
||||
|
cy.get(".appTable") |
||||
|
.within(() => { |
||||
|
cy.get(".spectrum-Icon").eq(1).click() |
||||
|
}) |
||||
|
// Check for when an app is published
|
||||
|
if (published == true) { |
||||
|
// Should not have Edit as option, will unpublish app
|
||||
|
cy.should("not.have.value", "Edit") |
||||
|
cy.get(".spectrum-Menu").contains("Unpublish").click() |
||||
|
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() |
||||
|
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click() |
||||
|
} |
||||
|
cy.contains("Edit").click() |
||||
|
cy.get(".spectrum-Modal") |
||||
|
.within(() => { |
||||
|
if (noName == true) { |
||||
|
cy.get("input").clear() |
||||
|
cy.get(".spectrum-Dialog-grid").click() |
||||
|
.contains("App name must be letters, numbers and spaces only") |
||||
|
return cy |
||||
|
} |
||||
|
cy.get("input").clear() |
||||
|
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur() |
||||
|
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true }) |
||||
|
cy.wait(500) |
||||
|
}) |
||||
|
} |
||||
}) |
}) |
||||
}) |
}) |
||||
|
|||||
@ -0,0 +1,118 @@ |
|||||
|
<script> |
||||
|
export let backgroundColour |
||||
|
export let imageSrc |
||||
|
export let name |
||||
|
export let icon |
||||
|
export let overlayEnabled = true |
||||
|
|
||||
|
let imageError = false |
||||
|
|
||||
|
const imageRenderError = () => { |
||||
|
imageError = true |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="template-card" style="background-color:{backgroundColour};"> |
||||
|
<div class="template-thumbnail card-body"> |
||||
|
<img |
||||
|
alt={name} |
||||
|
src={imageSrc} |
||||
|
on:error={imageRenderError} |
||||
|
class:error={imageError} |
||||
|
/> |
||||
|
<div style={`display:${imageError ? "block" : "none"}`}> |
||||
|
<svg |
||||
|
width="26px" |
||||
|
height="26px" |
||||
|
class="spectrum-Icon" |
||||
|
style="color: white" |
||||
|
focusable="false" |
||||
|
> |
||||
|
<use xlink:href="#spectrum-icon-18-{icon}" /> |
||||
|
</svg> |
||||
|
</div> |
||||
|
<div class={overlayEnabled ? "template-thumbnail-action-overlay" : ""}> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="template-thumbnail-text"> |
||||
|
<div>{name}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.template-thumbnail { |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.template-card:hover .template-thumbnail-action-overlay { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
.template-thumbnail-action-overlay { |
||||
|
position: absolute; |
||||
|
top: 0px; |
||||
|
left: 0px; |
||||
|
width: 100%; |
||||
|
height: 70%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background-color: rgba(0, 0, 0, 0.7); |
||||
|
opacity: 0; |
||||
|
transition: opacity var(--spectrum-global-animation-duration-100) ease; |
||||
|
border-top-right-radius: inherit; |
||||
|
border-top-left-radius: inherit; |
||||
|
} |
||||
|
|
||||
|
.template-thumbnail-text { |
||||
|
position: absolute; |
||||
|
bottom: 0px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
height: 30%; |
||||
|
width: 100%; |
||||
|
color: var( |
||||
|
--spectrum-heading-xs-text-color, |
||||
|
var(--spectrum-alias-heading-text-color) |
||||
|
); |
||||
|
background-color: var(--spectrum-global-color-gray-50); |
||||
|
} |
||||
|
|
||||
|
.template-thumbnail-text > div { |
||||
|
padding-left: 1rem; |
||||
|
padding-right: 1rem; |
||||
|
} |
||||
|
|
||||
|
.template-card { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
border-radius: var(--border-radius-s); |
||||
|
border: 1px solid var(--spectrum-global-color-gray-300); |
||||
|
overflow: hidden; |
||||
|
min-height: 200px; |
||||
|
} |
||||
|
|
||||
|
.template-card > * { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.template-card img { |
||||
|
display: block; |
||||
|
max-width: 100%; |
||||
|
border-radius: var(--border-radius-s) 0px var(--border-radius-s) 0px; |
||||
|
} |
||||
|
.template-card img.error { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.template-card:hover { |
||||
|
background: var(--spectrum-alias-background-color-tertiary); |
||||
|
} |
||||
|
|
||||
|
.card-body { |
||||
|
padding-left: 1rem; |
||||
|
padding-top: 1rem; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,152 @@ |
|||||
|
<script> |
||||
|
import { |
||||
|
Layout, |
||||
|
Detail, |
||||
|
Heading, |
||||
|
Button, |
||||
|
Modal, |
||||
|
ActionGroup, |
||||
|
ActionButton, |
||||
|
} from "@budibase/bbui" |
||||
|
import TemplateCard from "components/common/TemplateCard.svelte" |
||||
|
import CreateAppModal from "components/start/CreateAppModal.svelte" |
||||
|
|
||||
|
export let templates |
||||
|
|
||||
|
let selectedTemplateCategory |
||||
|
let creationModal |
||||
|
let template |
||||
|
|
||||
|
const groupTemplatesByCategory = (templates, categoryFilter) => { |
||||
|
let grouped = templates.reduce((acc, template) => { |
||||
|
if ( |
||||
|
typeof categoryFilter === "string" && |
||||
|
[categoryFilter].indexOf(template.category) < 0 |
||||
|
) { |
||||
|
return acc |
||||
|
} |
||||
|
|
||||
|
acc[template.category] = !acc[template.category] |
||||
|
? [] |
||||
|
: acc[template.category] |
||||
|
acc[template.category].push(template) |
||||
|
|
||||
|
return acc |
||||
|
}, {}) |
||||
|
return grouped |
||||
|
} |
||||
|
|
||||
|
$: filteredTemplates = groupTemplatesByCategory( |
||||
|
templates, |
||||
|
selectedTemplateCategory |
||||
|
) |
||||
|
|
||||
|
$: filteredTemplateCategories = filteredTemplates |
||||
|
? Object.keys(filteredTemplates).sort() |
||||
|
: [] |
||||
|
|
||||
|
$: templateCategories = templates |
||||
|
? Object.keys(groupTemplatesByCategory(templates)).sort() |
||||
|
: [] |
||||
|
|
||||
|
const stopAppCreation = () => { |
||||
|
template = null |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="template-header"> |
||||
|
<Layout noPadding gap="S"> |
||||
|
<Heading size="S">Templates</Heading> |
||||
|
<div class="template-category-filters spectrum-ActionGroup"> |
||||
|
<ActionGroup> |
||||
|
<ActionButton |
||||
|
selected={!selectedTemplateCategory} |
||||
|
on:click={() => { |
||||
|
selectedTemplateCategory = null |
||||
|
}} |
||||
|
> |
||||
|
All |
||||
|
</ActionButton> |
||||
|
{#each templateCategories as templateCategoryKey} |
||||
|
<ActionButton |
||||
|
dataCy={templateCategoryKey} |
||||
|
selected={templateCategoryKey == selectedTemplateCategory} |
||||
|
on:click={() => { |
||||
|
selectedTemplateCategory = templateCategoryKey |
||||
|
}} |
||||
|
> |
||||
|
{templateCategoryKey} |
||||
|
</ActionButton> |
||||
|
{/each} |
||||
|
</ActionGroup> |
||||
|
</div> |
||||
|
</Layout> |
||||
|
</div> |
||||
|
|
||||
|
<div class="template-categories"> |
||||
|
<Layout gap="XL" noPadding> |
||||
|
{#each filteredTemplateCategories as templateCategoryKey} |
||||
|
<div class="template-category" data-cy={templateCategoryKey}> |
||||
|
<Detail size="M">{templateCategoryKey}</Detail> |
||||
|
<div class="template-grid"> |
||||
|
{#each filteredTemplates[templateCategoryKey] as templateEntry} |
||||
|
<TemplateCard |
||||
|
name={templateEntry.name} |
||||
|
imageSrc={templateEntry.image} |
||||
|
backgroundColour={templateEntry.background} |
||||
|
icon={templateEntry.icon} |
||||
|
> |
||||
|
<Button |
||||
|
cta |
||||
|
on:click={() => { |
||||
|
template = templateEntry |
||||
|
creationModal.show() |
||||
|
}} |
||||
|
> |
||||
|
Use template |
||||
|
</Button> |
||||
|
<a |
||||
|
href={templateEntry.url} |
||||
|
target="_blank" |
||||
|
class="overlay-preview-link spectrum-Button spectrum-Button--sizeM spectrum-Button--secondary" |
||||
|
on:click|stopPropagation |
||||
|
> |
||||
|
Details |
||||
|
</a> |
||||
|
</TemplateCard> |
||||
|
{/each} |
||||
|
</div> |
||||
|
</div> |
||||
|
{/each} |
||||
|
</Layout> |
||||
|
</div> |
||||
|
|
||||
|
<Modal |
||||
|
bind:this={creationModal} |
||||
|
padding={false} |
||||
|
width="600px" |
||||
|
on:hide={stopAppCreation} |
||||
|
> |
||||
|
<CreateAppModal {template} /> |
||||
|
</Modal> |
||||
|
|
||||
|
<style> |
||||
|
.template-grid { |
||||
|
padding-top: 10px; |
||||
|
display: grid; |
||||
|
grid-gap: var(--spacing-xl); |
||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); |
||||
|
} |
||||
|
|
||||
|
a:hover.spectrum-Button.spectrum-Button--secondary.overlay-preview-link { |
||||
|
background-color: #c8c8c8; |
||||
|
border-color: #c8c8c8; |
||||
|
color: #505050; |
||||
|
} |
||||
|
|
||||
|
a.spectrum-Button--secondary.overlay-preview-link { |
||||
|
margin-top: 20px; |
||||
|
border-color: #c8c8c8; |
||||
|
color: #c8c8c8; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,78 @@ |
|||||
|
<script> |
||||
|
import { Select, Body } from "@budibase/bbui" |
||||
|
import { onMount } from "svelte" |
||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" |
||||
|
export let parameters |
||||
|
export let bindings |
||||
|
|
||||
|
const typeOptions = [ |
||||
|
{ |
||||
|
label: "Continue if", |
||||
|
value: "continue", |
||||
|
}, |
||||
|
{ |
||||
|
label: "Stop if", |
||||
|
value: "stop", |
||||
|
}, |
||||
|
] |
||||
|
const operatorOptions = [ |
||||
|
{ |
||||
|
label: "Equals", |
||||
|
value: "equal", |
||||
|
}, |
||||
|
{ |
||||
|
label: "Not equals", |
||||
|
value: "notEqual", |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
onMount(() => { |
||||
|
if (!parameters.type) { |
||||
|
parameters.type = "continue" |
||||
|
} |
||||
|
if (!parameters.operator) { |
||||
|
parameters.operator = "equal" |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<div class="root"> |
||||
|
<Body size="S"> |
||||
|
Configure a condition to be evaluated which can stop further actions from |
||||
|
being executed. |
||||
|
</Body> |
||||
|
<Select |
||||
|
bind:value={parameters.type} |
||||
|
options={typeOptions} |
||||
|
placeholder={null} |
||||
|
/> |
||||
|
<DrawerBindableInput |
||||
|
placeholder="Value" |
||||
|
value={parameters.value} |
||||
|
on:change={e => (parameters.value = e.detail)} |
||||
|
{bindings} |
||||
|
/> |
||||
|
<Select |
||||
|
bind:value={parameters.operator} |
||||
|
options={operatorOptions} |
||||
|
placeholder={null} |
||||
|
/> |
||||
|
<DrawerBindableInput |
||||
|
placeholder="Reference value" |
||||
|
bind:value={parameters.referenceValue} |
||||
|
on:change={e => (parameters.referenceValue = e.detail)} |
||||
|
{bindings} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.root { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: var(--spacing-l); |
||||
|
justify-content: flex-start; |
||||
|
align-items: stretch; |
||||
|
max-width: 400px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
</style> |
||||
@ -1,171 +0,0 @@ |
|||||
<script> |
|
||||
import analytics from "analytics" |
|
||||
import { createEventDispatcher } from "svelte" |
|
||||
import { fade, fly } from "svelte/transition" |
|
||||
import { |
|
||||
ActionButton, |
|
||||
ClearButton, |
|
||||
RadioGroup, |
|
||||
TextArea, |
|
||||
ButtonGroup, |
|
||||
Button, |
|
||||
Heading, |
|
||||
Detail, |
|
||||
Divider, |
|
||||
Layout, |
|
||||
notifications, |
|
||||
} from "@budibase/bbui" |
|
||||
import { auth } from "stores/portal" |
|
||||
|
|
||||
let step = 0 |
|
||||
let ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
|
||||
let options = [ |
|
||||
"Importing / managing data", |
|
||||
"Designing", |
|
||||
"Automations", |
|
||||
"Managing users / groups", |
|
||||
"Deployment / hosting", |
|
||||
"Documentation", |
|
||||
] |
|
||||
|
|
||||
const dispatch = createEventDispatcher() |
|
||||
|
|
||||
// Data to send off |
|
||||
let rating |
|
||||
let improvements = "" |
|
||||
let comment = "" |
|
||||
|
|
||||
function selectNumber(n) { |
|
||||
rating = n |
|
||||
step = 1 |
|
||||
} |
|
||||
|
|
||||
function submitFeedback() { |
|
||||
analytics.submitFeedback({ |
|
||||
rating, |
|
||||
improvements, |
|
||||
comment, |
|
||||
}) |
|
||||
try { |
|
||||
auth.updateSelf({ |
|
||||
flags: { |
|
||||
feedbackSubmitted: true, |
|
||||
}, |
|
||||
}) |
|
||||
} catch (error) { |
|
||||
notifications.error("Error updating user") |
|
||||
} |
|
||||
dispatch("complete") |
|
||||
} |
|
||||
|
|
||||
function cancelFeedback() { |
|
||||
try { |
|
||||
auth.updateSelf({ |
|
||||
flags: { |
|
||||
feedbackSubmitted: true, |
|
||||
}, |
|
||||
}) |
|
||||
} catch (error) { |
|
||||
notifications.error("Error updating user") |
|
||||
} |
|
||||
dispatch("complete") |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div |
|
||||
class="position" |
|
||||
in:fade={{ duration: 200 }} |
|
||||
out:fade|local={{ duration: 200 }} |
|
||||
> |
|
||||
<div |
|
||||
class="feedback-frame" |
|
||||
in:fly={{ y: 30, duration: 200 }} |
|
||||
out:fly|local={{ y: 30, duration: 200 }} |
|
||||
> |
|
||||
<div class="close"> |
|
||||
<ClearButton on:click={cancelFeedback} /> |
|
||||
</div> |
|
||||
<Layout gap="XS"> |
|
||||
{#if step === 0} |
|
||||
<Heading size="XS" |
|
||||
>How likely are you to recommend Budibase to a colleague?</Heading |
|
||||
> |
|
||||
<Divider /> |
|
||||
<div class="ratings"> |
|
||||
{#each ratings as number} |
|
||||
<ActionButton |
|
||||
size="L" |
|
||||
emphasized |
|
||||
selected={number === rating} |
|
||||
on:click={() => selectNumber(number)} |
|
||||
> |
|
||||
{number} |
|
||||
</ActionButton> |
|
||||
{/each} |
|
||||
</div> |
|
||||
<div class="footer"> |
|
||||
<Detail size="S">NOT LIKELY</Detail> |
|
||||
<Detail size="S">EXTREMELY LIKELY</Detail> |
|
||||
</div> |
|
||||
{:else if step === 1} |
|
||||
<Heading size="XS">What could be improved most in Budibase?</Heading> |
|
||||
<Divider /> |
|
||||
<RadioGroup bind:value={improvements} {options} /> |
|
||||
<div class="footer"> |
|
||||
<Detail size="S">STEP 2 OF 3</Detail> |
|
||||
<ButtonGroup> |
|
||||
<Button secondary on:click={() => (step -= 1)}>Previous</Button> |
|
||||
<Button primary on:click={() => (step += 1)}>Next</Button> |
|
||||
</ButtonGroup> |
|
||||
</div> |
|
||||
{:else} |
|
||||
<Heading size="XS">How can we improve your experience?</Heading> |
|
||||
<Divider /> |
|
||||
<TextArea bind:value={comment} placeholder="Add comments" /> |
|
||||
<div class="footer"> |
|
||||
<Detail size="S">STEP 3 OF 3</Detail> |
|
||||
<ButtonGroup> |
|
||||
<Button secondary on:click={() => (step -= 1)}>Previous</Button> |
|
||||
<Button cta on:click={submitFeedback}>Complete</Button> |
|
||||
</ButtonGroup> |
|
||||
</div> |
|
||||
{/if} |
|
||||
</Layout> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.feedback-frame :global(textarea) { |
|
||||
min-height: 180px !important; |
|
||||
} |
|
||||
|
|
||||
.position { |
|
||||
position: absolute; |
|
||||
right: var(--spacing-l); |
|
||||
bottom: calc(5 * var(--spacing-xl)); |
|
||||
} |
|
||||
.feedback-frame { |
|
||||
position: absolute; |
|
||||
bottom: 0; |
|
||||
right: 0; |
|
||||
min-width: 510px; |
|
||||
background: var(--background); |
|
||||
border-radius: var(--spectrum-global-dimension-size-50); |
|
||||
border: 2px solid var(--spectrum-global-color-blue-400); |
|
||||
padding: var(--spacing-xl); |
|
||||
} |
|
||||
.ratings { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
} |
|
||||
.close { |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
right: 0; |
|
||||
} |
|
||||
.footer { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
align-items: center; |
|
||||
} |
|
||||
</style> |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue