Merge branch 'feature/dependencies-image' into test/9339-sqlpostgres-row-api-test-suite

This commit is contained in:
adrinr 2023-02-02 10:12:33 +00:00
commit 4eb0c07953
380 changed files with 10496 additions and 9200 deletions

View File

@ -18,30 +18,18 @@ jobs:
- run: yarn
- run: yarn bootstrap
- run: yarn build
- name: Pull cypress.env.yaml from budibase-infra
- name: Pull from budibase-infra
run: |
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \
-o packages/builder/cypress.env.json \
-L https://api.github.com/repos/budibase/budibase-infra/contents/test/cypress.env.json
wc -l packages/builder/cypress.env.json
- name: Cypress run
id: cypress
continue-on-error: true
uses: cypress-io/github-action@v2
with:
record: true
install: false
tag: nightly
command: yarn test:e2e:ci:record
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
-o
-L
wc -l
- uses: actions/upload-artifact@v3
with:
name: Test Reports
path: packages/builder/cypress/reports/testReport.html
path:
# TODO: enable once running in QA test env
# - name: Configure AWS Credentials
@ -54,11 +42,3 @@ jobs:
# - name: Upload test results HTML
# uses: aws-actions/configure-aws-credentials@v1
# run: aws s3 cp packages/builder/cypress/reports/testReport.html s3://{{ secrets.BUDI_QA_REPORTS_BUCKET_NAME }}/$GITHUB_RUN_ID/index.html
- name: Cypress Discord Notify
run: yarn test:e2e:ci:notify
env:
CYPRESS_WEBHOOK_URL: ${{ secrets.BUDI_QA_WEBHOOK }}
CYPRESS_OUTCOME: ${{ steps.cypress.outcome }}
CYPRESS_DASHBOARD_URL: ${{ steps.cypress.outputs.dashboardUrl }}
GITHUB_RUN_URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID

View File

@ -139,6 +139,8 @@ spec:
value: {{ .Values.globals.automationMaxIterations | quote }}
- name: TENANT_FEATURE_FLAGS
value: {{ .Values.globals.tenantFeatureFlags | quote }}
- name: ENCRYPTION_KEY
value: {{ .Values.globals.bbEncryptionKey | quote }}
{{ if .Values.globals.bbAdminUserEmail }}
- name: BB_ADMIN_USER_EMAIL
value: {{ .Values.globals.bbAdminUserEmail | quote }}

View File

@ -146,6 +146,8 @@ spec:
value: {{ .Values.globals.google.secret | quote }}
- name: TENANT_FEATURE_FLAGS
value: {{ .Values.globals.tenantFeatureFlags | quote }}
- name: ENCRYPTION_KEY
value: {{ .Values.globals.bbEncryptionKey | quote }}
{{ if .Values.globals.elasticApmEnabled }}
- name: ELASTIC_APM_ENABLED
value: {{ .Values.globals.elasticApmEnabled | quote }}

View File

@ -0,0 +1,32 @@
FROM couchdb:3.2.1
ENV COUCHDB_USER admin
ENV COUCHDB_PASSWORD admin
EXPOSE 5984
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \
apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \
apt-get update && apt-get install -y --no-install-recommends openjdk-8-jre && \
rm -rf /var/lib/apt/lists/
# setup clouseau
WORKDIR /
RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip && \
unzip clouseau-2.21.0-dist.zip && \
mv clouseau-2.21.0 /opt/clouseau && \
rm clouseau-2.21.0-dist.zip
WORKDIR /opt/clouseau
RUN mkdir ./bin
ADD clouseau/clouseau ./bin/
ADD clouseau/log4j.properties clouseau/clouseau.ini ./
# setup CouchDB
WORKDIR /opt/couchdb
ADD couch/vm.args couch/local.ini ./etc/
WORKDIR /
ADD build-target-paths.sh .
ADD runner.sh ./bbcouch-runner.sh
RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau ./build-target-paths.sh
CMD ["./bbcouch-runner.sh"]

View File

@ -0,0 +1,24 @@
#!/bin/bash
echo ${TARGETBUILD} > /buildtarget.txt
if [[ "${TARGETBUILD}" = "aas" ]]; then
# Azure AppService uses /home for persisent data & SSH on port 2222
DATA_DIR=/home
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
mkdir -p $DATA_DIR/{search,minio,couch}
mkdir -p $DATA_DIR/couch/{dbs,views}
chown -R couchdb:couchdb $DATA_DIR/couch/
apt update
apt-get install -y openssh-server
echo "root:Docker!" | chpasswd
mkdir -p /tmp
chmod +x /tmp/ssh_setup.sh \
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
cp /etc/sshd_config /etc/ssh/sshd_config
/etc/init.d/ssh restart
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
else
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
fi

14
hosting/couchdb/runner.sh Normal file
View File

@ -0,0 +1,14 @@
#!/bin/bash
DATA_DIR=${DATA_DIR:-/data}
mkdir -p ${DATA_DIR}
mkdir -p ${DATA_DIR}/couch/{dbs,views}
mkdir -p ${DATA_DIR}/search
chown -R couchdb:couchdb ${DATA_DIR}/couch
/build-target-paths.sh
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
sleep 10
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator
sleep infinity

View File

@ -0,0 +1,23 @@
FROM budibase/couchdb
ENV DATA_DIR /data
RUN mkdir /data
RUN apt-get update && \
apt-get install -y --no-install-recommends redis-server
WORKDIR /minio
ADD scripts/install-minio.sh ./install.sh
RUN chmod +x install.sh && ./install.sh
WORKDIR /
ADD dependencies/runner.sh .
RUN chmod +x ./runner.sh
EXPOSE 5984
EXPOSE 9000
EXPOSE 9001
EXPOSE 6379
CMD ["./runner.sh"]

View File

@ -0,0 +1,57 @@
# Docker Image for Running Budibase Tests
## Overview
This image contains the basic setup for running
## Usage
- Build the Image
- Run the Container
### Build the Image
The guidance below is based on building the Budibase single image on Debian 11 and AlmaLinux 8. If you use another distro or OS you will need to amend the commands to suit.
#### Install Node
Budibase requires a more recent version of node (14+) than is available in the base Debian repos so:
```
curl -sL https://deb.nodesource.com/setup_16.x | sudo bash -
apt install -y nodejs
node -v
```
Install yarn and lerna:
```
npm install -g yarn jest lerna
```
#### Install Docker
```
apt install -y docker.io
```
Check the versions of each installed version. This process was tested with the version numbers below so YMMV using anything else:
- Docker: 20.10.5
- node: 16.15.1
- yarn: 1.22.19
- lerna: 5.1.4
#### Get the Code
Clone the Budibase repo
```
git clone https://github.com/Budibase/budibase.git
cd budibase
```
#### Setup Node
Node setup:
```
node ./hosting/scripts/setup.js
yarn
yarn bootstrap
yarn build
```
#### Build Image
The following yarn command does some prep and then runs the docker build command:
```
yarn build:docker:dependencies
```

View File

@ -0,0 +1,8 @@
#!/bin/bash
redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 &
/bbcouch-runner.sh &
/minio/minio server ${DATA_DIR}/minio --console-address ":9001" > /dev/stdout 2>&1 &
echo "Test environment started..."
sleep infinity

View File

@ -42,17 +42,17 @@ services:
couchdb-service:
# platform: linux/amd64
container_name: budi-couchdb-dev
container_name: budi-couchdb3-dev
restart: on-failure
image: ibmcom/couchdb3
image: budibase/couchdb
environment:
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
- COUCHDB_USER=${COUCH_DB_USER}
ports:
- "${COUCH_DB_PORT}:5984"
volumes:
- couchdb3_data:/opt/couchdb/data
- couchdb_data:/data
couch-init:
container_name: budi-couchdb-init-dev
image: curlimages/curl
@ -60,7 +60,12 @@ services:
PUT_CALL: "curl -u ${COUCH_DB_USER}:${COUCH_DB_PASSWORD} -X PUT couchdb-service:5984"
depends_on:
- couchdb-service
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:
container_name: budi-redis-dev
@ -73,7 +78,7 @@ services:
- redis_data:/data
volumes:
couchdb3_data:
couchdb_data:
driver: local
minio_data:
driver: local

View File

@ -0,0 +1,47 @@
version: "3"
# optional ports are specified throughout for more advanced use cases.
services:
minio-service:
restart: on-failure
# Last version that supports the "fs" backend
image: minio/minio:RELEASE.2022-10-24T18-35-07Z
ports:
- 9000
- 9001
environment:
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
couchdb-service:
# platform: linux/amd64
restart: on-failure
image: budibase/couchdb
environment:
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
- COUCHDB_USER=${COUCH_DB_USER}
ports:
- 5984
- 4369
- 9100
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5984/_up"]
interval: 30s
timeout: 20s
retries: 3
redis-service:
restart: on-failure
image: redis
command: redis-server --requirepass ${REDIS_PASSWORD}
ports:
- 6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]

View File

@ -0,0 +1,10 @@
#!/bin/bash
if [[ $TARGETARCH == arm* ]] ;
then
echo "INSTALLING ARM64 MINIO"
wget https://dl.min.io/server/minio/release/linux-arm64/minio
else
echo "INSTALLING AMD64 MINIO"
wget https://dl.min.io/server/minio/release/linux-amd64/minio
fi
chmod +x minio

View File

@ -0,0 +1,15 @@
#!/bin/bash
tag=$1
if [[ ! "$tag" ]]; then
echo "No tag present. You must pass a tag to this script"
exit 1
fi
echo "Tagging images with tag: $tag"
docker tag budibase-couchdb budibase/couchdb:$tag
docker push --all-tags budibase/couchdb

View File

@ -18,7 +18,7 @@ WORKDIR /worker
ADD packages/worker .
RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
FROM couchdb:3.2.1
FROM budibase/couchdb
ARG TARGETARCH
ENV TARGETARCH $TARGETARCH
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
@ -29,23 +29,9 @@ ENV TARGETBUILD $TARGETBUILD
COPY --from=build /app /app
COPY --from=build /worker /worker
# ENV CUSTOM_DOMAIN=budi001.custom.com \
# See runner.sh for Env Vars
# These secret env variables are generated by the runner at startup
# their values can be overriden by the user, they will be written
# to the .env file in the /data directory for use later on
# REDIS_PASSWORD=budibase \
# COUCHDB_PASSWORD=budibase \
# COUCHDB_USER=budibase \
# COUCH_DB_URL=http://budibase:budibase@localhost:5984 \
# INTERNAL_API_KEY=budibase \
# JWT_SECRET=testsecret \
# MINIO_ACCESS_KEY=budibase \
# MINIO_SECRET_KEY=budibase \
# install base dependencies
RUN apt-get update && \
apt-get install -y software-properties-common wget nginx uuid-runtime && \
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server && \
apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \
apt-get update
@ -53,7 +39,7 @@ RUN apt-get update && \
WORKDIR /nodejs
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \
bash /tmp/nodesource_setup.sh && \
apt-get install -y libaio1 nodejs nginx openjdk-8-jdk redis-server unzip && \
apt-get install -y --no-install-recommends libaio1 nodejs && \
npm install --global yarn pm2
# setup nginx
@ -69,23 +55,6 @@ RUN mkdir -p scripts/integrations/oracle
ADD packages/server/scripts/integrations/oracle scripts/integrations/oracle
RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh
# setup clouseau
WORKDIR /
RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip && \
unzip clouseau-2.21.0-dist.zip && \
mv clouseau-2.21.0 /opt/clouseau && \
rm clouseau-2.21.0-dist.zip
WORKDIR /opt/clouseau
RUN mkdir ./bin
ADD hosting/single/clouseau/clouseau ./bin/
ADD hosting/single/clouseau/log4j.properties hosting/single/clouseau/clouseau.ini ./
RUN chmod +x ./bin/clouseau
# setup CouchDB
WORKDIR /opt/couchdb
ADD hosting/single/couch/vm.args hosting/single/couch/local.ini ./etc/
# setup minio
WORKDIR /minio
ADD scripts/install-minio.sh ./install.sh
@ -98,9 +67,6 @@ RUN chmod +x ./runner.sh
ADD hosting/single/healthcheck.sh .
RUN chmod +x ./healthcheck.sh
ADD hosting/scripts/build-target-paths.sh .
RUN chmod +x ./build-target-paths.sh
# Script below sets the path for storing data based on $DATA_DIR
# For Azure App Service install SSH & point data locations to /home
ADD hosting/single/ssh/sshd_config /etc/

View File

@ -72,14 +72,11 @@ for LINE in $(cat ${DATA_DIR}/.env); do export $LINE; done
ln -s ${DATA_DIR}/.env /app/.env
ln -s ${DATA_DIR}/.env /worker/.env
# make these directories in runner, incase of mount
mkdir -p ${DATA_DIR}/couch/{dbs,views}
mkdir -p ${DATA_DIR}/minio
mkdir -p ${DATA_DIR}/search
chown -R couchdb:couchdb ${DATA_DIR}/couch
redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 &
/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 &
/bbcouch-runner.sh &
/minio/minio server --console-address ":9001" ${DATA_DIR}/minio > /dev/stdout 2>&1 &
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
/etc/init.d/nginx restart
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
# Add monthly cron job to renew certbot certificate
@ -90,15 +87,14 @@ if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
/etc/init.d/nginx restart
fi
# wait for backend services to start
sleep 10
pushd app
pm2 start -l /dev/stdout --name app "yarn run:docker"
popd
pushd worker
pm2 start -l /dev/stdout --name worker "yarn run:docker"
popd
sleep 10
echo "curl to couchdb endpoints"
curl -X PUT ${COUCH_DB_URL}/_users
curl -X PUT ${COUCH_DB_URL}/_replicator
echo "end of runner.sh, sleeping ..."
sleep infinity

View File

@ -0,0 +1,9 @@
module.exports = () => {
return {
dockerCompose: {
composeFilePath: "../../hosting",
composeFile: "docker-compose.test.yaml",
startupTimeout: 10000,
},
}
}

View File

@ -1,5 +1,5 @@
{
"version": "2.2.12-alpha.28",
"version": "2.2.12-alpha.59",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -52,10 +52,6 @@
"lint:fix:eslint": "eslint --fix packages qa-core",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"test:e2e": "lerna run cy:test --stream",
"test:e2e:ci": "lerna run cy:ci --stream",
"test:e2e:ci:record": "lerna run cy:ci:record --stream",
"test:e2e:ci:notify": "lerna run cy:ci:notify",
"build:specs": "lerna run specs",
"build:docker": "lerna run build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:pre": "lerna run build && lerna run predocker",
@ -67,6 +63,9 @@
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
"build:docker:single": "npm run build:docker:pre && npm run build:docker:single:image",
"build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting",
"publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.2.1 --push ./hosting/couchdb",
"publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting",
"build:docs": "lerna run build:docs",
"release:helm": "node scripts/releaseHelmChart",
"env:multi:enable": "lerna run env:multi:enable",
@ -85,4 +84,4 @@
"install:pro": "bash scripts/pro/install.sh",
"dep:clean": "yarn clean && yarn bootstrap"
}
}
}

View File

@ -0,0 +1,8 @@
const { join } = require("path")
require("dotenv").config({
path: join(__dirname, "..", "..", "hosting", ".env"),
})
const jestTestcontainersConfigGenerator = require("../../jestTestcontainersConfigGenerator")
module.exports = jestTestcontainersConfigGenerator()

View File

@ -1,14 +1,40 @@
import { Config } from "@jest/types"
import { Config } from "jest"
const preset = require("ts-jest/jest-preset")
const config: Config.InitialOptions = {
preset: "ts-jest",
testEnvironment: "node",
setupFiles: ["./tests/jestSetup.ts"],
collectCoverageFrom: ["src/**/*.{js,ts}"],
coverageReporters: ["lcov", "json", "clover"],
const testContainersSettings = {
...preset,
preset: "@trendyol/jest-testcontainers",
setupFiles: ["./tests/jestEnv.ts"],
setupFilesAfterEnv: ["./tests/jestSetup.ts"],
transform: {
"^.+\\.ts?$": "@swc/jest",
},
}
if (!process.env.CI) {
// use sources when not in CI
testContainersSettings.moduleNameMapper = {
"@budibase/types": "<rootDir>/../types/src",
}
} else {
console.log("Running tests with compiled dependency sources")
}
const config: Config = {
projects: [
{
...testContainersSettings,
displayName: "sequential test",
testMatch: ["<rootDir>/**/*.seq.spec.[jt]s"],
runner: "jest-serial-runner",
},
{
...testContainersSettings,
testMatch: ["<rootDir>/**/!(*.seq).spec.[jt]s"],
},
],
collectCoverageFrom: ["src/**/*.{js,ts}"],
coverageReporters: ["lcov", "json", "clover"],
}
export default config

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "2.2.12-alpha.28",
"version": "2.2.12-alpha.59",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@ -23,7 +23,7 @@
},
"dependencies": {
"@budibase/nano": "10.1.1",
"@budibase/types": "2.2.12-alpha.28",
"@budibase/types": "2.2.12-alpha.59",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0",
@ -59,6 +59,7 @@
"devDependencies": {
"@swc/core": "^1.3.25",
"@swc/jest": "^0.2.24",
"@trendyol/jest-testcontainers": "^2.1.1",
"@types/chance": "1.1.3",
"@types/ioredis": "4.28.0",
"@types/jest": "27.5.1",
@ -76,6 +77,7 @@
"chance": "1.1.8",
"ioredis-mock": "5.8.0",
"jest": "28.1.1",
"jest-serial-runner": "^1.2.1",
"koa": "2.13.4",
"nodemon": "2.0.16",
"pouchdb-adapter-memory": "7.2.2",
@ -86,4 +88,4 @@
"typescript": "4.7.3"
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}
}

View File

@ -9,16 +9,8 @@ import {
jwt as jwtPassport,
local,
authenticated,
auditLog,
tenancy,
authError,
ssoCallbackUrl,
csrf,
internalApi,
adminOnly,
builderOnly,
builderOrAdmin,
joiValidator,
oidc,
google,
} from "../middleware"

View File

@ -1,4 +1,4 @@
require("../../../tests")
const{generator}=require("../../../tests")
const { Writethrough } = require("../writethrough")
const { getDB } = require("../../db")
const tk = require("timekeeper")
@ -6,10 +6,12 @@ const tk = require("timekeeper")
const START_DATE = Date.now()
tk.freeze(START_DATE)
const { newid } = require("../../newid")
const DELAY = 5000
const db = getDB("test")
const db2 = getDB("test2")
const db = getDB(`db_${newid()}`)
const db2 = getDB(`db_${newid()}`)
const writethrough = new Writethrough(db, DELAY), writethrough2 = new Writethrough(db2, DELAY)
describe("writethrough", () => {

View File

@ -77,6 +77,7 @@ export const StaticDatabases = {
apiKeys: "apikeys",
usageQuota: "usage_quota",
licenseInfo: "license_info",
environmentVariables: "environmentvariables",
},
},
// contains information about tenancy and so on

View File

@ -1,17 +1,14 @@
import { AsyncLocalStorage } from "async_hooks"
import { ContextMap } from "./mainContext"
export default class Context {
static storage = new AsyncLocalStorage<Record<string, any>>()
static storage = new AsyncLocalStorage<ContextMap>()
static run(context: Record<string, any>, func: any) {
static run(context: ContextMap, func: any) {
return Context.storage.run(context, () => func())
}
static get(): Record<string, any> {
return Context.storage.getStore() as Record<string, any>
}
static set(context: Record<string, any>) {
Context.storage.enterWith(context)
static get(): ContextMap {
return Context.storage.getStore() as ContextMap
}
}

View File

@ -16,6 +16,7 @@ export type ContextMap = {
tenantId?: string
appId?: string
identity?: IdentityContext
environmentVariables?: Record<string, string>
}
let TEST_APP_ID: string | null = null
@ -75,7 +76,7 @@ export function getTenantIDFromAppID(appId: string) {
}
}
function updateContext(updates: ContextMap) {
function updateContext(updates: ContextMap): ContextMap {
let context: ContextMap
try {
context = Context.get()
@ -120,15 +121,23 @@ export async function doInTenant(
return newContext(updates, task)
}
export async function doInAppContext(appId: string, task: any): Promise<any> {
if (!appId) {
export async function doInAppContext(
appId: string | null,
task: any
): Promise<any> {
if (!appId && !env.isTest()) {
throw new Error("appId is required")
}
const tenantId = getTenantIDFromAppID(appId)
const updates: ContextMap = { appId }
if (tenantId) {
updates.tenantId = tenantId
let updates: ContextMap
if (!appId) {
updates = { appId: "" }
} else {
const tenantId = getTenantIDFromAppID(appId)
updates = { appId }
if (tenantId) {
updates.tenantId = tenantId
}
}
return newContext(updates, task)
}
@ -189,25 +198,25 @@ export const getProdAppId = () => {
return conversions.getProdAppID(appId)
}
export function updateTenantId(tenantId?: string) {
let context: ContextMap = updateContext({
tenantId,
})
Context.set(context)
export function doInEnvironmentContext(
values: Record<string, string>,
task: any
) {
if (!values) {
throw new Error("Must supply environment variables.")
}
const updates = {
environmentVariables: values,
}
return newContext(updates, task)
}
export function updateAppId(appId: string) {
let context: ContextMap = updateContext({
appId,
})
try {
Context.set(context)
} catch (err) {
if (env.isTest()) {
TEST_APP_ID = appId
} else {
throw err
}
export function getEnvironmentVariables() {
const context = Context.get()
if (!context.environmentVariables) {
return null
} else {
return context.environmentVariables
}
}

View File

@ -15,18 +15,47 @@ import { getCouchInfo } from "./connections"
import { directCouchCall } from "./utils"
import { getPouchDB } from "./pouchDB"
import { WriteStream, ReadStream } from "fs"
import { newid } from "../../newid"
function buildNano(couchInfo: { url: string; cookie: string }) {
return Nano({
url: couchInfo.url,
requestDefaults: {
headers: {
Authorization: couchInfo.cookie,
},
},
parseUrl: false,
})
}
export function DatabaseWithConnection(
dbName: string,
connection: string,
opts?: DatabaseOpts
) {
if (!connection) {
throw new Error("Must provide connection details")
}
return new DatabaseImpl(dbName, opts, connection)
}
export class DatabaseImpl implements Database {
public readonly name: string
private static nano: Nano.ServerScope
private readonly instanceNano?: Nano.ServerScope
private readonly pouchOpts: DatabaseOpts
constructor(dbName?: string, opts?: DatabaseOpts) {
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
if (dbName == null) {
throw new Error("Database name cannot be undefined.")
}
this.name = dbName
this.pouchOpts = opts || {}
if (connection) {
const couchInfo = getCouchInfo(connection)
this.instanceNano = buildNano(couchInfo)
}
if (!DatabaseImpl.nano) {
DatabaseImpl.init()
}
@ -34,15 +63,7 @@ export class DatabaseImpl implements Database {
static init() {
const couchInfo = getCouchInfo()
DatabaseImpl.nano = Nano({
url: couchInfo.url,
requestDefaults: {
headers: {
Authorization: couchInfo.cookie,
},
},
parseUrl: false,
})
DatabaseImpl.nano = buildNano(couchInfo)
}
async exists() {
@ -50,6 +71,10 @@ export class DatabaseImpl implements Database {
return response.status === 200
}
private nano() {
return this.instanceNano || DatabaseImpl.nano
}
async checkSetup() {
let shouldCreate = !this.pouchOpts?.skip_setup
// check exists in a lightweight fashion
@ -58,9 +83,16 @@ export class DatabaseImpl implements Database {
throw new Error("DB does not exist")
}
if (!exists) {
await DatabaseImpl.nano.db.create(this.name)
try {
await this.nano().db.create(this.name)
} catch (err: any) {
// Handling race conditions
if (err.statusCode !== 412) {
throw err
}
}
}
return DatabaseImpl.nano.db.use(this.name)
return this.nano().db.use(this.name)
}
private async updateOutput(fnc: any) {
@ -101,6 +133,13 @@ export class DatabaseImpl implements Database {
return this.updateOutput(() => db.destroy(_id, _rev))
}
async post(document: AnyDocument, opts?: DatabasePutOpts) {
if (!document._id) {
document._id = newid()
}
return this.put(document, opts)
}
async put(document: AnyDocument, opts?: DatabasePutOpts) {
if (!document._id) {
throw new Error("Cannot store document without _id field.")
@ -146,7 +185,7 @@ export class DatabaseImpl implements Database {
async destroy() {
try {
await DatabaseImpl.nano.db.destroy(this.name)
return await this.nano().db.destroy(this.name)
} catch (err: any) {
// didn't exist, don't worry
if (err.statusCode === 404) {

View File

@ -1,7 +1,7 @@
import env from "../../environment"
export const getCouchInfo = () => {
const urlInfo = getUrlInfo()
export const getCouchInfo = (connection?: string) => {
const urlInfo = getUrlInfo(connection)
let username
let password
if (env.COUCH_DB_USERNAME) {

View File

@ -6,12 +6,6 @@ import { DatabaseImpl } from "../db"
const dbList = new Set()
export function getDB(dbName?: string, opts?: any): Database {
// TODO: once using the test image, need to remove this
if (env.isTest()) {
dbList.add(dbName)
// @ts-ignore
return getPouchDB(dbName, opts)
}
return new DatabaseImpl(dbName, opts)
}

View File

@ -1,19 +1,19 @@
require("../../../tests")
const { getDB } = require("../")
const { newid } = require("../../newid")
const { getDB } = require("../db")
describe("db", () => {
describe("db", () => {
describe("getDB", () => {
it("returns a db", async () => {
const db = getDB("test")
const dbName = `db_${newid()}`
const db = getDB(dbName)
expect(db).toBeDefined()
expect(db._adapter).toBe("memory")
expect(db.prefix).toBe("_pouch_")
expect(db.name).toBe("test")
expect(db.name).toBe(dbName)
})
it("uses the custom put function", async () => {
const db = getDB("test")
const db = getDB(`db_${newid()}`)
let doc = { _id: "test" }
await db.put(doc)
doc = await db.get(doc._id)
@ -23,4 +23,3 @@ describe("db", () => {
})
})
})

View File

@ -8,6 +8,7 @@ const {
const { generateAppID, getPlatformUrl, getScopedConfig } = require("../utils")
const tenancy = require("../../tenancy")
const { Config, DEFAULT_TENANT_ID } = require("../../constants")
import { generator } from "../../../tests"
import env from "../../environment"
describe("utils", () => {
@ -66,17 +67,16 @@ describe("utils", () => {
})
})
const DB_URL = "http://dburl.com"
const DEFAULT_URL = "http://localhost:10000"
const ENV_URL = "http://env.com"
const setDbPlatformUrl = async () => {
const setDbPlatformUrl = async (dbUrl: string) => {
const db = tenancy.getGlobalDB()
db.put({
await db.put({
_id: "config_settings",
type: Config.SETTINGS,
config: {
platformUrl: DB_URL,
platformUrl: dbUrl,
},
})
}
@ -119,9 +119,10 @@ describe("getPlatformUrl", () => {
it("gets the platform url from the database", async () => {
await tenancy.doInTenant(null, async () => {
await setDbPlatformUrl()
const dbUrl = generator.url()
await setDbPlatformUrl(dbUrl)
const url = await getPlatformUrl()
expect(url).toBe(DB_URL)
expect(url).toBe(dbUrl)
})
})
})
@ -152,7 +153,7 @@ describe("getPlatformUrl", () => {
it("never gets the platform url from the database", async () => {
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
await setDbPlatformUrl()
await setDbPlatformUrl(generator.url())
const url = await getPlatformUrl()
expect(url).toBe(TENANT_AWARE_URL)
})
@ -170,10 +171,11 @@ describe("getScopedConfig", () => {
it("returns the platform url with an existing config", async () => {
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
await setDbPlatformUrl()
const dbUrl = generator.url()
await setDbPlatformUrl(dbUrl)
const db = tenancy.getGlobalDB()
const config = await getScopedConfig(db, { type: Config.SETTINGS })
expect(config.platformUrl).toBe(DB_URL)
expect(config.platformUrl).toBe(dbUrl)
})
})

View File

@ -10,7 +10,7 @@ import {
APP_PREFIX,
} from "../constants"
import { getTenantId, getGlobalDB, getGlobalDBName } from "../context"
import { doWithDB, allDbs, directCouchAllDbs } from "./db"
import { doWithDB, directCouchAllDbs } from "./db"
import { getAppMetadata } from "../cache/appMetadata"
import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
import * as events from "../events"
@ -262,10 +262,7 @@ export function getStartEndKeyURL(baseKey: any, tenantId?: string) {
*/
export async function getAllDbs(opts = { efficient: false }) {
const efficient = opts && opts.efficient
// specifically for testing we use the pouch package for this
if (env.isTest()) {
return allDbs()
}
let dbs: any[] = []
async function addDbs(queryString?: string) {
const json = await directCouchAllDbs(queryString)

View File

@ -48,6 +48,7 @@ const environment = {
},
JS_BCRYPT: process.env.JS_BCRYPT,
JWT_SECRET: process.env.JWT_SECRET,
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,

View File

@ -0,0 +1,38 @@
import {
Event,
EnvironmentVariableCreatedEvent,
EnvironmentVariableDeletedEvent,
EnvironmentVariableUpgradePanelOpenedEvent,
} from "@budibase/types"
import { publishEvent } from "../events"
async function created(name: string, environments: string[]) {
const properties: EnvironmentVariableCreatedEvent = {
name,
environments,
}
await publishEvent(Event.ENVIRONMENT_VARIABLE_CREATED, properties)
}
async function deleted(name: string) {
const properties: EnvironmentVariableDeletedEvent = {
name,
}
await publishEvent(Event.ENVIRONMENT_VARIABLE_DELETED, properties)
}
async function upgradePanelOpened(userId: string) {
const properties: EnvironmentVariableUpgradePanelOpenedEvent = {
userId,
}
await publishEvent(
Event.ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED,
properties
)
}
export default {
created,
deleted,
upgradePanelOpened,
}

View File

@ -20,3 +20,4 @@ export { default as backfill } from "./backfill"
export { default as group } from "./group"
export { default as plugin } from "./plugin"
export { default as backup } from "./backup"
export { default as environmentVariable } from "./environmentVariable"

View File

@ -13,6 +13,7 @@ import {
UserPermissionAssignedEvent,
UserPermissionRemovedEvent,
UserUpdatedEvent,
UserOnboardingEvent,
} from "@budibase/types"
async function created(user: User, timestamp?: number) {
@ -36,6 +37,13 @@ async function deleted(user: User) {
await publishEvent(Event.USER_DELETED, properties)
}
export async function onboardingComplete(user: User) {
const properties: UserOnboardingEvent = {
userId: user._id as string,
}
await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties)
}
// PERMISSIONS
async function permissionAdminAssigned(user: User, timestamp?: number) {
@ -126,6 +134,7 @@ export default {
permissionAdminRemoved,
permissionBuilderAssigned,
permissionBuilderRemoved,
onboardingComplete,
invited,
inviteAccepted,
passwordForceReset,

View File

@ -2,7 +2,7 @@ import { newid } from "./utils"
import * as events from "./events"
import { StaticDatabases } from "./db"
import { doWithDB } from "./db"
import { Installation, IdentityType } from "@budibase/types"
import { Installation, IdentityType, Database } from "@budibase/types"
import * as context from "./context"
import semver from "semver"
import { bustCache, withCache, TTL, CacheKey } from "./cache/generic"
@ -14,6 +14,24 @@ export const getInstall = async (): Promise<Installation> => {
useTenancy: false,
})
}
async function createInstallDoc(platformDb: Database) {
const install: Installation = {
_id: StaticDatabases.PLATFORM_INFO.docs.install,
installId: newid(),
version: pkg.version,
}
try {
const resp = await platformDb.put(install)
install._rev = resp.rev
return install
} catch (err: any) {
if (err.status === 409) {
return getInstallFromDB()
} else {
throw err
}
}
}
const getInstallFromDB = async (): Promise<Installation> => {
return doWithDB(
@ -26,13 +44,7 @@ const getInstallFromDB = async (): Promise<Installation> => {
)
} catch (e: any) {
if (e.status === 404) {
install = {
_id: StaticDatabases.PLATFORM_INFO.docs.install,
installId: newid(),
version: pkg.version,
}
const resp = await platformDb.put(install)
install._rev = resp.rev
install = await createInstallDoc(platformDb)
} else {
throw e
}

View File

@ -64,7 +64,7 @@ const print = (fn: any, data: any[]) => {
message = message + ` [identityId=${identityId}]`
}
fn(message, data)
// fn(message, data)
}
const logging = (ctx: any, next: any) => {

View File

@ -6,6 +6,8 @@ const { DEFAULT_TENANT_ID } = require("../../../constants")
const { generateGlobalUserID } = require("../../../db/utils")
const { newid } = require("../../../utils")
const { doWithGlobalDB, doInTenant } = require("../../../tenancy")
const { default: environment } = require("../../../environment")
environment._set("MULTI_TENANCY", 'TRUE')
const done = jest.fn()

View File

@ -3,7 +3,7 @@
exports[`migrations should match snapshot 1`] = `
Object {
"_id": "migrations",
"_rev": "1-a32b0b708e59eeb006ed5e063cfeb36a",
"_rev": "1-2f64479842a0513aa8b97f356b0b9127",
"createdAt": "2020-01-01T00:00:00.000Z",
"test": 1577836800000,
"updatedAt": "2020-01-01T00:00:00.000Z",

View File

@ -1,9 +1,10 @@
require("../../../tests")
const { runMigrations, getMigrationsDoc } = require("../index")
const { getDB } = require("../../db")
const {
StaticDatabases,
} = require("../../constants")
const { getGlobalDBName, getDB } = require("../../db")
const { default: environment } = require("../../environment")
const { newid } = require("../../newid")
environment._set("MULTI_TENANCY", 'TRUE')
let db
@ -17,8 +18,11 @@ describe("migrations", () => {
fn: migrationFunction
}]
let tenantId
beforeEach(() => {
db = getDB(StaticDatabases.GLOBAL.name)
tenantId = `tenant_${newid()}`
db = getDB(getGlobalDBName(tenantId))
})
afterEach(async () => {
@ -27,7 +31,7 @@ describe("migrations", () => {
})
const migrate = () => {
return runMigrations(MIGRATIONS)
return runMigrations(MIGRATIONS, { tenantIds: [tenantId]})
}
it("should run a new migration", async () => {

View File

@ -2,19 +2,45 @@ import crypto from "crypto"
import env from "../environment"
const ALGO = "aes-256-ctr"
const SECRET = env.JWT_SECRET
const SEPARATOR = "-"
const ITERATIONS = 10000
const RANDOM_BYTES = 16
const STRETCH_LENGTH = 32
export enum SecretOption {
JWT = "jwt",
ENCRYPTION = "encryption",
}
function getSecret(secretOption: SecretOption): string {
let secret, secretName
switch (secretOption) {
case SecretOption.ENCRYPTION:
secret = env.ENCRYPTION_KEY
secretName = "ENCRYPTION_KEY"
break
case SecretOption.JWT:
default:
secret = env.JWT_SECRET
secretName = "JWT_SECRET"
break
}
if (!secret) {
throw new Error(`Secret "${secretName}" has not been set in environment.`)
}
return secret
}
function stretchString(string: string, salt: Buffer) {
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
}
export function encrypt(input: string) {
export function encrypt(
input: string,
secretOption: SecretOption = SecretOption.JWT
) {
const salt = crypto.randomBytes(RANDOM_BYTES)
const stretched = stretchString(SECRET!, salt)
const stretched = stretchString(getSecret(secretOption), salt)
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
const base = cipher.update(input)
const final = cipher.final()
@ -22,10 +48,13 @@ export function encrypt(input: string) {
return `${salt.toString("hex")}${SEPARATOR}${encrypted}`
}
export function decrypt(input: string) {
export function decrypt(
input: string,
secretOption: SecretOption = SecretOption.JWT
) {
const [salt, encrypted] = input.split(SEPARATOR)
const saltBuffer = Buffer.from(salt, "hex")
const stretched = stretchString(SECRET!, saltBuffer)
const stretched = stretchString(getSecret(secretOption), saltBuffer)
const decipher = crypto.createDecipheriv(ALGO, stretched, saltBuffer)
const base = decipher.update(Buffer.from(encrypted, "hex"))
const final = decipher.final()

View File

@ -2,13 +2,19 @@ import { structures } from "../../../tests"
import * as utils from "../../utils"
import * as events from "../../events"
import * as db from "../../db"
import { DEFAULT_TENANT_ID, Header } from "../../constants"
import { Header } from "../../constants"
import { doInTenant } from "../../context"
import environment from "../../environment"
import { newid } from "../../utils"
describe("utils", () => {
describe("platformLogout", () => {
beforeEach(() => {
environment._set("MULTI_TENANCY", "TRUE")
})
it("should call platform logout", async () => {
await doInTenant(DEFAULT_TENANT_ID, async () => {
await doInTenant(`tenant-${newid()}`, async () => {
const ctx = structures.koa.newContext()
await utils.platformLogout({ ctx, userId: "test" })
expect(events.auth.logout).toBeCalledTimes(1)
@ -17,6 +23,10 @@ describe("utils", () => {
})
describe("getAppIdFromCtx", () => {
beforeEach(() => {
environment._set("MULTI_TENANCY", undefined)
})
it("gets appId from header", async () => {
const ctx = structures.koa.newContext()
const expected = db.generateAppID()
@ -54,7 +64,7 @@ describe("utils", () => {
const app = structures.apps.app(expected)
// set custom url
const appUrl = "custom-url"
const appUrl = newid()
app.url = `/${appUrl}`
ctx.path = `/app/${appUrl}`

View File

@ -0,0 +1,23 @@
import env from "../src/environment"
import { mocks } from "./utilities"
// must explicitly enable fetch mock
mocks.fetch.enable()
// mock all dates to 2020-01-01T00:00:00.000Z
// use tk.reset() to use real dates in individual tests
import tk from "timekeeper"
tk.freeze(mocks.date.MOCK_DATE)
env._set("SELF_HOSTED", "1")
env._set("NODE_ENV", "jest")
if (!process.env.DEBUG) {
global.console.log = jest.fn() // console.log are ignored in tests
}
if (!process.env.CI) {
// set a longer timeout in dev for debugging
// 100 seconds
jest.setTimeout(100000)
}

View File

@ -1,28 +1,4 @@
import env from "../src/environment"
import { mocks } from "./utilities"
import { testContainerUtils } from "./utilities"
// must explicitly enable fetch mock
mocks.fetch.enable()
// mock all dates to 2020-01-01T00:00:00.000Z
// use tk.reset() to use real dates in individual tests
import tk from "timekeeper"
tk.freeze(mocks.date.MOCK_DATE)
env._set("SELF_HOSTED", "1")
env._set("NODE_ENV", "jest")
env._set("JWT_SECRET", "test-jwtsecret")
env._set("LOG_LEVEL", "silent")
env._set("MINIO_URL", "http://localhost")
env._set("MINIO_ACCESS_KEY", "test")
env._set("MINIO_SECRET_KEY", "test")
if (!process.env.DEBUG) {
global.console.log = jest.fn() // console.log are ignored in tests
}
if (!process.env.CI) {
// set a longer timeout in dev for debugging
// 100 seconds
jest.setTimeout(100000)
}
testContainerUtils.setupEnv(env)

View File

@ -2,6 +2,7 @@ export * as mocks from "./mocks"
export * as structures from "./structures"
export { generator } from "./structures"
export * as testEnv from "./testEnv"
export * as testContainerUtils from "./testContainerUtils"
import * as dbConfig from "./db"
dbConfig.init()

View File

@ -0,0 +1,42 @@
function getTestContainerSettings(serverName: string, key: string) {
const entry = Object.entries(global).find(
([k]) =>
k.includes(`_${serverName.toUpperCase()}`) &&
k.includes(`_${key.toUpperCase()}__`)
)
if (!entry) {
return null
}
return entry[1]
}
function getCouchConfig() {
const port = getTestContainerSettings("COUCHDB-SERVICE", "PORT_5984")
return {
port,
url: `http://${getTestContainerSettings("COUCHDB-SERVICE", "IP")}:${port}`,
}
}
function getMinioConfig() {
const port = getTestContainerSettings("MINIO-SERVICE", "PORT_9000")
return {
port,
url: `http://${getTestContainerSettings("MINIO-SERVICE", "IP")}:${port}`,
}
}
export function setupEnv(...envs: any[]) {
const configs = [
{ key: "COUCH_DB_PORT", value: getCouchConfig().port },
{ key: "COUCH_DB_URL", value: getCouchConfig().url },
{ key: "MINIO_PORT", value: getMinioConfig().port },
{ key: "MINIO_URL", value: getMinioConfig().url },
]
for (const config of configs.filter(x => x.value !== null)) {
for (const env of envs) {
env._set(config.key, config.value)
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,58 @@
<script>
import "@spectrum-css/accordion"
export let itemName
export let initialOpen
export let header
let isOpen
function getOpenClass(isOpen) {
if (isOpen === undefined) {
isOpen = initialOpen
}
return isOpen ? "is-open" : ""
}
</script>
<div class="spectrum-Accordion" role={itemName}>
<div class="spectrum-Accordion-item {getOpenClass(isOpen)}">
<h3 class="spectrum-Accordion-itemHeading">
<button
class="spectrum-Accordion-itemHeader"
type="button"
on:click={() => (isOpen = !isOpen)}
>
{header}
</button>
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronRight100 spectrum-Accordion-itemIndicator"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</h3>
<div class="spectrum-Accordion-itemContent" role={itemName}>
<slot />
</div>
</div>
</div>
<style>
.spectrum-Accordion {
margin-left: -20px;
}
.spectrum-Accordion-item {
border: none;
}
.spectrum-Accordion-itemContent {
width: 97%;
padding-left: 30px;
}
.spectrum-Accordion-itemHeader {
text-transform: none;
font-weight: bold;
font-size: 0.875rem;
}
</style>

View File

@ -9,7 +9,6 @@
export let longPressable = false
export let disabled = false
export let icon = ""
export let dataCy = null
export let size = "M"
export let active = false
export let fullWidth = false
@ -37,7 +36,6 @@
</script>
<button
data-cy={dataCy}
use:longPress
class:spectrum-ActionButton--quiet={quiet}
class:spectrum-ActionButton--emphasized={emphasized}
@ -86,8 +84,9 @@
margin-left: 0;
transition: color ease-out 130ms;
}
.is-selected:not(.spectrum-ActionButton--emphasized) {
.is-selected:not(.spectrum-ActionButton--emphasized):not(.spectrum-ActionButton--quiet) {
background: var(--spectrum-global-color-gray-300);
border-color: var(--spectrum-global-color-gray-700);
}
.noPadding {
padding: 0;

View File

@ -6,7 +6,6 @@
export let disabled = false
export let align = "left"
export let portalTarget
export let dataCy
let anchor
let dropdown
@ -37,7 +36,7 @@
<div use:getAnchor on:click={openMenu}>
<slot name="control" />
</div>
<Popover bind:this={dropdown} {anchor} {align} {portalTarget} {dataCy}>
<Popover bind:this={dropdown} {anchor} {align} {portalTarget}>
<Menu>
<slot />
</Menu>

View File

@ -1,4 +1,4 @@
const ignoredClasses = [".flatpickr-calendar"]
const ignoredClasses = [".flatpickr-calendar", ".spectrum-Popover"]
let clickHandlers = []
/**
@ -19,7 +19,7 @@ const handleClick = event => {
}
// Ignore clicks for modals, unless the handler is registered from a modal
const sourceInModal = handler.element.closest(".spectrum-Modal") != null
const sourceInModal = handler.anchor.closest(".spectrum-Modal") != null
const clickInModal = event.target.closest(".spectrum-Modal") != null
if (clickInModal && !sourceInModal) {
return
@ -33,10 +33,10 @@ document.documentElement.addEventListener("click", handleClick, true)
/**
* Adds or updates a click handler
*/
const updateHandler = (id, element, callback) => {
const updateHandler = (id, element, anchor, callback) => {
let existingHandler = clickHandlers.find(x => x.id === id)
if (!existingHandler) {
clickHandlers.push({ id, element, callback })
clickHandlers.push({ id, element, anchor, callback })
} else {
existingHandler.callback = callback
}
@ -51,12 +51,22 @@ const removeHandler = id => {
/**
* Svelte action to apply a click outside handler for a certain element
* opts.anchor is an optional param specifying the real root source of the
* component being observed. This is required for things like popovers, where
* the element using the clickoutside action is the popover, but the popover is
* rendered at the root of the DOM somewhere, whereas the popover anchor is the
* element we actually want to consider when determining the source component.
*/
export default (element, callback) => {
export default (element, opts) => {
const id = Math.random()
updateHandler(id, element, callback)
const update = newOpts => {
const callback = newOpts?.callback || newOpts
const anchor = newOpts?.anchor || element
updateHandler(id, element, anchor, callback)
}
update(opts)
return {
update: newCallback => updateHandler(id, element, newCallback),
update,
destroy: () => removeHandler(id),
}
}

View File

@ -1,8 +1,11 @@
export default function positionDropdown(
element,
{ anchor, align, maxWidth, useAnchorWidth }
{ anchor, align, maxWidth, useAnchorWidth, offset = 5 }
) {
const update = () => {
if (!anchor) {
return
}
const anchorBounds = anchor.getBoundingClientRect()
const elementBounds = element.getBoundingClientRect()
let styles = {
@ -14,10 +17,12 @@ export default function positionDropdown(
}
// Determine vertical styles
if (window.innerHeight - anchorBounds.bottom < 100) {
styles.top = anchorBounds.top - elementBounds.height - 5
if (align === "right-outside") {
styles.top = anchorBounds.top
} else if (window.innerHeight - anchorBounds.bottom < 100) {
styles.top = anchorBounds.top - elementBounds.height - offset
} else {
styles.top = anchorBounds.bottom + 5
styles.top = anchorBounds.bottom + offset
styles.maxHeight = window.innerHeight - anchorBounds.bottom - 20
}
@ -30,8 +35,8 @@ export default function positionDropdown(
}
if (align === "right") {
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
} else if (align === "right-side") {
styles.left = anchorBounds.left + anchorBounds.width
} else if (align === "right-outside") {
styles.left = anchorBounds.right + offset
} else {
styles.left = anchorBounds.left
}
@ -54,8 +59,11 @@ export default function positionDropdown(
const resizeObserver = new ResizeObserver(entries => {
entries.forEach(update)
})
resizeObserver.observe(anchor)
if (anchor) {
resizeObserver.observe(anchor)
}
resizeObserver.observe(element)
resizeObserver.observe(document.body)
document.addEventListener("scroll", update, true)

View File

@ -13,13 +13,14 @@
export let icon = undefined
export let active = false
export let tooltip = undefined
export let dataCy
export let newStyles = true
export let id
let showTooltip = false
</script>
<button
{id}
class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary}
@ -31,7 +32,6 @@
class:disabled
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
{disabled}
data-cy={dataCy}
on:click|preventDefault
on:mouseover={() => (showTooltip = true)}
on:focus={() => (showTooltip = true)}

View File

@ -0,0 +1,30 @@
<script>
import Icon from "../Icon/Icon.svelte"
import FancyField from "./FancyField.svelte"
export let icon
export let disabled
</script>
<FancyField on:click clickable {disabled}>
{#if icon}
{#if icon.includes("/")}
<img src={icon} alt="button" />
{:else}
<Icon name={icon} />
{/if}
{/if}
<slot name="icon" />
<div>
<slot />
</div>
</FancyField>
<style>
img {
width: 22px;
}
div {
font-size: var(--font-size-l);
}
</style>

View File

@ -0,0 +1,70 @@
<script>
import { createEventDispatcher } from "svelte"
import FancyField from "./FancyField.svelte"
import FancyFieldLabel from "./FancyFieldLabel.svelte"
import ActionButton from "../ActionButton/ActionButton.svelte"
export let label
export let value
export let disabled = false
export let error = null
export let validate = null
export let options = []
export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value")
const dispatch = createEventDispatcher()
$: placeholder = !value
const extractProperty = (value, property) => {
if (value && typeof value === "object") {
return value[property]
}
return value
}
const onChange = newValue => {
dispatch("change", newValue)
value = newValue
if (validate) {
error = validate(newValue)
}
}
</script>
<FancyField {error} {value} {validate} {disabled} autoHeight>
{#if label}
<FancyFieldLabel placeholder={false}>{label}</FancyFieldLabel>
{/if}
<div class="options">
{#each options as option}
<ActionButton
selected={getOptionValue(option) === value}
on:click={() => onChange(getOptionValue(option))}
>
{getOptionLabel(option)}
</ActionButton>
{/each}
</div>
</FancyField>
<style>
.options {
margin-top: 34px;
margin-bottom: 14px;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.options :global(.spectrum-ActionButton) {
font-size: 15px;
line-height: 17px;
height: auto;
padding: 6px 10px;
}
</style>

View File

@ -0,0 +1,53 @@
<script>
import { createEventDispatcher } from "svelte"
import FancyField from "./FancyField.svelte"
import Checkbox from "../Form/Core/Checkbox.svelte"
export let value
export let text
export let disabled = false
export let error = null
export let validate = null
const dispatch = createEventDispatcher()
const onChange = () => {
const newValue = !value
dispatch("change", newValue)
value = newValue
if (validate) {
error = validate(newValue)
}
}
</script>
<FancyField {error} {value} {validate} {disabled} clickable on:click={onChange}>
<span>
<Checkbox {disabled} {value} />
</span>
<div class="text">
{#if text}
{text}
{/if}
<slot />
</div>
</FancyField>
<style>
span {
pointer-events: none;
}
.text {
font-size: 15px;
line-height: 17px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.text > :global(*) {
font-size: inherit !important;
}
</style>

View File

@ -0,0 +1,126 @@
<script>
import Icon from "../Icon/Icon.svelte"
import { getContext, onMount } from "svelte"
import { slide } from "svelte/transition"
export let disabled = false
export let error = null
export let focused = false
export let clickable = false
export let validate
export let value
export let ref
export let autoHeight
const formContext = getContext("fancy-form")
const id = Math.random()
const API = {
validate: () => {
if (validate) {
error = validate(value)
}
return !error
},
}
onMount(() => {
if (formContext) {
formContext.registerField(id, API)
}
return () => {
if (formContext) {
formContext.unregisterField(id)
}
}
})
</script>
<div
bind:this={ref}
class="fancy-field"
class:error
class:disabled
class:focused
class:clickable
class:auto-height={autoHeight}
>
<div class="content" on:click>
<div class="field">
<slot />
</div>
{#if error}
<div class="error-icon">
<Icon name="Alert" />
</div>
{/if}
</div>
{#if error}
<div transition:slide|local={{ duration: 130 }} class="error-message">
{error}
</div>
{/if}
</div>
<style>
.fancy-field {
max-width: 400px;
background: var(--spectrum-global-color-gray-75);
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
box-sizing: border-box;
transition: border-color 130ms ease-out, background 130ms ease-out,
background 130ms ease-out;
color: var(--spectrum-global-color-gray-800);
}
.fancy-field:hover {
border-color: var(--spectrum-global-color-gray-400);
}
.fancy-field.clickable:hover {
background: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
.fancy-field.focused {
border-color: var(--spectrum-global-color-blue-400);
}
.fancy-field.error {
border-color: var(--spectrum-global-color-red-400);
}
.fancy-field.disabled {
background: var(--spectrum-global-color-gray-200);
color: var(--spectrum-global-color-gray-400);
border: 1px solid var(--spectrum-global-color-gray-300);
pointer-events: none;
}
.content {
position: relative;
height: 64px;
padding: 0 16px;
}
.fancy-field.auto-height .content {
height: auto;
}
.content,
.field {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 16px;
}
.field {
flex: 1 1 auto;
}
.error-message {
background: var(--spectrum-global-color-red-400);
color: white;
font-size: 14px;
padding: 6px 16px;
font-weight: 500;
}
.error-icon {
flex: 0 0 auto;
}
.error-icon :global(.spectrum-Icon) {
fill: var(--spectrum-global-color-red-400);
}
</style>

View File

@ -0,0 +1,25 @@
<script>
export let placeholder = true
</script>
<div class:placeholder>
<slot />
</div>
<style>
div {
font-size: 14px;
line-height: 15px;
font-weight: 500;
position: absolute;
top: 10px;
color: var(--spectrum-global-color-gray-600);
transition: font-size 130ms ease-out, top 130ms ease-out,
transform 130ms ease-out;
}
div.placeholder {
top: 50%;
font-size: 15px;
transform: translateY(-50%);
}
</style>

View File

@ -0,0 +1,40 @@
<script>
import { setContext } from "svelte"
let fields = {}
setContext("fancy-form", {
registerField: (id, api) => {
fields = { ...fields, [id]: api }
},
unregisterField: id => {
delete fields[id]
fields = fields
},
})
export const validate = () => {
let valid = true
Object.values(fields).forEach(api => {
if (!api.validate()) {
valid = false
}
})
return valid
}
</script>
<div class="fancy-form">
<slot />
</div>
<style>
.fancy-form :global(.fancy-field:not(:first-of-type)) {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.fancy-form :global(.fancy-field:not(:last-of-type)) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
</style>

View File

@ -0,0 +1,77 @@
<script>
import { createEventDispatcher } from "svelte"
import FancyField from "./FancyField.svelte"
import FancyFieldLabel from "./FancyFieldLabel.svelte"
import { fade } from "svelte/transition"
export let label
export let value
export let type = "text"
export let disabled = false
export let error = null
export let validate = null
export let suffix = null
const dispatch = createEventDispatcher()
let focused = false
$: placeholder = !focused && !value
const onChange = e => {
const newValue = e.target.value
dispatch("change", newValue)
value = newValue
if (validate) {
error = validate(newValue)
}
}
</script>
<FancyField {error} {value} {validate} {disabled} {focused}>
{#if label}
<FancyFieldLabel {placeholder}>{label}</FancyFieldLabel>
{/if}
<input
{disabled}
value={value || ""}
type={type || "text"}
on:input={onChange}
on:focus={() => (focused = true)}
on:blur={() => (focused = false)}
class:placeholder
/>
{#if suffix && !placeholder}
<div in:fade|local={{ duration: 130 }} class="suffix">{suffix}</div>
{/if}
</FancyField>
<style>
input {
width: 100%;
transition: transform 130ms ease-out;
transform: translateY(9px);
background: transparent;
font-size: 15px;
line-height: 17px;
font-family: var(--font-sans);
color: var(--spectrum-global-color-gray-900);
outline: none;
border: none;
padding: 0;
margin: 0;
}
input.placeholder {
transform: translateY(0);
position: absolute;
top: 0;
left: 0;
height: 100%;
}
.suffix {
color: var(--spectrum-global-color-gray-600);
transform: translateY(9px);
font-size: 15px;
line-height: 17px;
font-family: var(--font-sans);
}
</style>

View File

@ -0,0 +1,147 @@
<script>
import { createEventDispatcher } from "svelte"
import FancyField from "./FancyField.svelte"
import Icon from "../Icon/Icon.svelte"
import Popover from "../Popover/Popover.svelte"
import FancyFieldLabel from "./FancyFieldLabel.svelte"
export let label
export let value
export let disabled = false
export let error = null
export let validate = null
export let options = []
export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value")
const dispatch = createEventDispatcher()
let open = false
let popover
let wrapper
$: placeholder = !value
$: selectedLabel = getSelectedLabel(value)
const extractProperty = (value, property) => {
if (value && typeof value === "object") {
return value[property]
}
return value
}
const onChange = newValue => {
dispatch("change", newValue)
value = newValue
if (validate) {
error = validate(newValue)
}
open = false
}
const getSelectedLabel = value => {
if (!value || !options?.length) {
return ""
}
const selectedOption = options.find(x => getOptionValue(x) === value)
if (!selectedOption) {
return value
}
return getOptionLabel(selectedOption)
}
</script>
<FancyField
bind:ref={wrapper}
{error}
{value}
{validate}
{disabled}
clickable
on:click={() => (open = true)}
>
{#if label}
<FancyFieldLabel {placeholder}>{label}</FancyFieldLabel>
{/if}
<div class="value" class:placeholder>
{selectedLabel || ""}
</div>
<div class="arrow">
<Icon name="ChevronDown" />
</div>
</FancyField>
<Popover
anchor={wrapper}
align="left"
portalTarget={document.documentElement}
bind:this={popover}
{open}
on:close={() => (open = false)}
useAnchorWidth={true}
maxWidth={null}
>
<div class="popover-content">
{#if options.length}
{#each options as option, idx}
<div
class="popover-option"
tabindex="0"
on:click={() => onChange(getOptionValue(option, idx))}
>
<span class="option-text">
{getOptionLabel(option, idx)}
</span>
{#if value === getOptionValue(option, idx)}
<Icon name="Checkmark" />
{/if}
</div>
{/each}
{/if}
</div>
</Popover>
<style>
.value {
display: block;
flex: 1 1 auto;
font-size: 15px;
line-height: 17px;
color: var(--spectrum-global-color-gray-900);
transition: transform 130ms ease-out, opacity 130ms ease-out;
opacity: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 0;
transform: translateY(9px);
}
.value.placeholder {
transform: translateY(0);
opacity: 0;
pointer-events: none;
margin-top: 0;
}
.popover-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
padding: 7px 0;
}
.popover-option {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 7px 16px;
transition: background 130ms ease-out;
font-size: 15px;
}
.popover-option:hover {
background: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
</style>

View File

@ -0,0 +1,6 @@
export { default as FancyInput } from "./FancyInput.svelte"
export { default as FancyCheckbox } from "./FancyCheckbox.svelte"
export { default as FancySelect } from "./FancySelect.svelte"
export { default as FancyButton } from "./FancyButton.svelte"
export { default as FancyForm } from "./FancyForm.svelte"
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"

View File

@ -0,0 +1,276 @@
<script>
import "@spectrum-css/textfield/dist/index-vars.css"
import { createEventDispatcher, onMount } from "svelte"
import clickOutside from "../../Actions/click_outside"
import Divider from "../../Divider/Divider.svelte"
export let value = null
export let placeholder = null
export let type = "text"
export let disabled = false
export let id = null
export let readonly = false
export let updateOnChange = true
export let align
export let autofocus = false
export let variables
export let showModal
export let environmentVariablesEnabled
export let handleUpgradePanel
const dispatch = createEventDispatcher()
let field
let focus = false
let iconFocused = false
let open = false
//eslint-disable-next-line
const STRIP_NAME_REGEX = /(?<=\.)(.*?)(?=\ })/g
// Strips the name out of the value which is {{ env.Variable }} resulting in an array like ["Variable"]
$: hbsValue = String(value)?.match(STRIP_NAME_REGEX) || []
const updateValue = newValue => {
if (readonly) {
return
}
if (type === "number") {
const float = parseFloat(newValue)
newValue = isNaN(float) ? null : float
}
dispatch("change", newValue)
}
const onFocus = () => {
if (readonly) {
return
}
focus = true
}
const onBlur = event => {
if (readonly) {
return
}
focus = false
updateValue(event.target.value)
}
const onInput = event => {
if (readonly || !updateOnChange) {
return
}
updateValue(event.target.value)
}
const handleOutsideClick = event => {
if (open) {
event.stopPropagation()
open = false
focus = false
iconFocused = false
dispatch("closed")
}
}
const handleVarSelect = variable => {
open = false
focus = false
iconFocused = false
updateValue(`{{ env.${variable} }}`)
}
onMount(() => {
focus = autofocus
if (focus) field.focus()
})
function removeVariable() {
updateValue("")
}
function openPopover() {
open = true
focus = true
iconFocused = true
}
</script>
<div class="spectrum-InputGroup">
<div
class:is-disabled={disabled || hbsValue.length}
class:is-focused={focus}
class="spectrum-Textfield"
>
<svg
class:close-color={hbsValue.length}
class:focused={iconFocused}
class="hoverable icon-position spectrum-Icon spectrum-Icon--sizeS spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
on:click={() => {
hbsValue.length ? removeVariable() : openPopover()
}}
>
<use
xlink:href={`#spectrum-icon-18-${!hbsValue.length ? "Key" : "Close"}`}
/>
</svg>
<input
bind:this={field}
disabled={hbsValue.length || disabled}
{readonly}
{id}
value={hbsValue.length ? `{{ ${hbsValue[0]} }}` : value}
placeholder={placeholder || ""}
on:click
on:blur
on:focus
on:input
on:keyup
on:blur={onBlur}
on:focus={onFocus}
on:input={onInput}
type={hbsValue.length ? "text" : type}
style={align ? `text-align: ${align};` : ""}
class="spectrum-Textfield-input"
inputmode={type === "number" ? "decimal" : "text"}
/>
</div>
{#if open}
<div
use:clickOutside={handleOutsideClick}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
>
<ul class="spectrum-Menu" role="listbox">
{#if !environmentVariablesEnabled}
<div class="no-variables-text primary-text">
Upgrade your plan to get environment variables
</div>
{:else if variables.length}
<div style="max-height: 100px">
{#each variables as variable, idx}
<li
class="spectrum-Menu-item"
role="option"
aria-selected="true"
tabindex="0"
on:click={() => handleVarSelect(variable.name)}
>
<span class="spectrum-Menu-itemLabel">
<div class="primary-text">
{variable.name}
<span />
</div>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</span>
</li>
{/each}
</div>
{:else}
<div class="no-variables-text primary-text">
You don't have any environment variables yet
</div>
{/if}
</ul>
<Divider noMargin />
{#if environmentVariablesEnabled}
<div on:click={() => showModal()} class="add-variable">
<svg
class="spectrum-Icon spectrum-Icon--sizeS "
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Add" />
</svg>
<div class="primary-text">Add Variable</div>
</div>
{:else}
<div on:click={() => handleUpgradePanel()} class="add-variable">
<svg
class="spectrum-Icon spectrum-Icon--sizeS "
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-ArrowUp" />
</svg>
<div class="primary-text">Upgrade plan</div>
</div>
{/if}
</div>
{/if}
</div>
<style>
.spectrum-Textfield {
width: 100%;
}
.icon-position {
position: absolute;
top: 25%;
right: 2%;
}
.hoverable:hover {
cursor: pointer;
color: var(--spectrum-global-color-blue-400);
}
.primary-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.spectrum-InputGroup {
min-width: 0;
width: 100%;
}
.spectrum-Popover {
max-height: 240px;
z-index: 999;
top: 100%;
}
.spectrum-Popover.spectrum-Popover--bottom.spectrum-Picker-popover.is-open {
width: 100%;
}
.no-variables-text {
padding: var(--spacing-m);
color: var(--spectrum-global-color-gray-600);
}
.add-variable {
display: flex;
padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-m);
align-items: center;
gap: var(--spacing-s);
cursor: pointer;
}
.focused {
color: var(--spectrum-global-color-blue-400);
}
.add-variable:hover {
background: var(--grey-1);
}
.close-color {
color: var(--spectrum-global-color-gray-900) !important;
}
.close-color:hover {
color: var(--spectrum-global-color-blue-400) !important;
}
</style>

View File

@ -45,7 +45,9 @@
getOptionLabel
)
const onClick = () => {
const onClick = e => {
e.preventDefault()
e.stopPropagation()
dispatch("click")
if (readonly) {
return
@ -88,7 +90,6 @@
class:is-open={open}
aria-haspopup="listbox"
on:click={onClick}
use:clickOutside={() => (open = false)}
bind:this={button}
>
{#if fieldIcon}
@ -130,14 +131,17 @@
<Popover
anchor={button}
align="left"
portalTarget={document.documentElement}
bind:this={popover}
{open}
on:close={() => (open = false)}
useAnchorWidth={!autoWidth}
maxWidth={autoWidth ? 400 : null}
>
<div class="popover-content" class:auto-width={autoWidth}>
<div
class="popover-content"
class:auto-width={autoWidth}
use:clickOutside={() => (open = false)}
>
{#if autocomplete}
<Search
value={searchTerm}

View File

@ -18,8 +18,11 @@
export let autoWidth = false
export let autocomplete = false
export let sort = false
const dispatch = createEventDispatcher()
let open = false
$: fieldText = getFieldText(value, options, placeholder)
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
$: fieldColour = getFieldAttribute(getOptionColour, value, options)

View File

@ -6,7 +6,6 @@
export let id = null
export let text = null
export let disabled = false
export let dataCy = null
const dispatch = createEventDispatcher()
const onChange = event => {
@ -16,7 +15,6 @@
<div class="spectrum-Switch spectrum-Switch--emphasized">
<input
data-cy={dataCy}
checked={value}
{disabled}
on:change={onChange}

View File

@ -11,7 +11,6 @@
export let readonly = false
export let updateOnChange = true
export let quiet = false
export let dataCy
export let align
export let autofocus = false
@ -89,7 +88,6 @@
{disabled}
{readonly}
{id}
data-cy={dataCy}
value={value || ""}
placeholder={placeholder || ""}
on:click

View File

@ -0,0 +1,50 @@
<script>
import Field from "./Field.svelte"
import EnvDropdown from "./Core/EnvDropdown.svelte"
import { createEventDispatcher } from "svelte"
export let value = null
export let label = null
export let labelPosition = "above"
export let placeholder = null
export let type = "text"
export let disabled = false
export let readonly = false
export let error = null
export let updateOnChange = true
export let quiet = false
export let autofocus
export let variables
export let showModal
export let environmentVariablesEnabled
export let handleUpgradePanel
const dispatch = createEventDispatcher()
const onChange = e => {
value = e.detail
dispatch("change", e.detail)
}
</script>
<Field {label} {labelPosition} {error}>
<EnvDropdown
{updateOnChange}
{error}
{disabled}
{readonly}
{value}
{placeholder}
{type}
{quiet}
{autofocus}
{variables}
{showModal}
{environmentVariablesEnabled}
{handleUpgradePanel}
on:change={onChange}
on:click
on:input
on:blur
on:focus
on:keyup
/>
</Field>

View File

@ -13,7 +13,6 @@
export let error = null
export let updateOnChange = true
export let quiet = false
export let dataCy
export let autofocus
const dispatch = createEventDispatcher()
@ -25,7 +24,6 @@
<Field {label} {labelPosition} {error}>
<TextField
{dataCy}
{updateOnChange}
{error}
{disabled}

View File

@ -14,7 +14,6 @@
export let error = null
export let updateOnChange = true
export let quiet = false
export let dataCy
export let autofocus
export let options = []
@ -32,7 +31,6 @@
<Field {label} {labelPosition} {error}>
<InputDropdown
{dataCy}
{updateOnChange}
{error}
{disabled}

View File

@ -21,7 +21,6 @@
export let getSecondaryOptionColour = () => {}
export let getSecondaryOptionIcon = () => {}
export let quiet = false
export let dataCy
export let autofocus
export let primaryOptions = []
export let secondaryOptions = []
@ -98,7 +97,6 @@
<PickerDropdown
{searchTerm}
{autocomplete}
{dataCy}
{error}
{disabled}
{readonly}

View File

@ -9,7 +9,6 @@
export let text = null
export let disabled = false
export let error = null
export let dataCy = null
const dispatch = createEventDispatcher()
const onChange = e => {
@ -19,5 +18,5 @@
</script>
<Field {label} {labelPosition} {error}>
<Switch {dataCy} {error} {disabled} {text} {value} on:change={onChange} />
<Switch {error} {disabled} {text} {value} on:change={onChange} />
</Field>

View File

@ -18,6 +18,7 @@
<div class="main">
<div class="content" class:wide class:noPadding class:narrow>
<slot />
<div class="fix-scroll-padding" />
</div>
</div>
<div
@ -55,9 +56,14 @@
max-width: 1080px;
margin: 0 auto;
flex: 1 1 auto;
padding: 50px;
padding: 50px 50px 0 50px;
z-index: 1;
}
.fix-scroll-padding {
content: "";
display: block;
flex: 0 0 50px;
}
.content.wide {
max-width: none;
}

View File

@ -1,5 +1,6 @@
<script>
import "@spectrum-css/link/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
export let href = "#"
export let size = "M"
@ -9,10 +10,12 @@
export let overBackground = false
export let target
export let download
const dispatch = createEventDispatcher()
</script>
<a
on:click
on:click={e => dispatch("click") && e.stopPropagation()}
{href}
{target}
{download}

View File

@ -5,7 +5,6 @@
const dispatch = createEventDispatcher()
const actionMenu = getContext("actionMenu")
export let dataCy
export let icon = undefined
export let disabled = undefined
export let noClose = false
@ -35,7 +34,6 @@
</script>
<li
data-cy={dataCy}
on:click|preventDefault={disabled ? null : onClick}
class="spectrum-Menu-item"
class:is-disabled={disabled}

View File

@ -23,7 +23,7 @@
export let secondaryButtonText = undefined
export let secondaryAction = undefined
export let secondaryButtonWarning = false
export let dataCy = null
const { hide, cancel } = getContext(Context.Modal)
let loading = false
$: confirmDisabled = disabled || loading
@ -63,7 +63,6 @@
role="dialog"
tabindex="-1"
aria-modal="true"
data-cy={dataCy}
>
<div class="spectrum-Dialog-grid">
{#if title || $$slots.header}

View File

@ -5,25 +5,21 @@
import positionDropdown from "../Actions/position_dropdown"
import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition"
import { getContext } from "svelte"
import Context from "../context"
const dispatch = createEventDispatcher()
export let anchor
export let align = "right"
export let portalTarget
export let dataCy
export let maxWidth
export let direction = "bottom"
export let showTip = false
export let open = false
export let useAnchorWidth = false
export let dismissible = true
export let offset = 5
let tipSvg =
'<svg xmlns="http://www.w3.org/svg/2000" width="23" height="12" class="spectrum-Popover-tip" > <path class="spectrum-Popover-tip-triangle" d="M 0.7071067811865476 0 L 11.414213562373096 10.707106781186548 L 22.121320343559645 0" /> </svg>'
$: tooltipClasses = showTip
? `spectrum-Popover--withTip spectrum-Popover--${direction}`
: ""
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
export const show = () => {
dispatch("open")
@ -61,38 +57,36 @@
</script>
{#if open}
<Portal target={portalTarget}>
<div
tabindex="0"
use:positionDropdown={{ anchor, align, maxWidth, useAnchorWidth }}
use:clickOutside={handleOutsideClick}
on:keydown={handleEscape}
class={"spectrum-Popover is-open " + (tooltipClasses || "")}
role="presentation"
data-cy={dataCy}
transition:fly|local={{ y: -20, duration: 200 }}
>
{#if showTip}
{@html tipSvg}
{/if}
<slot />
</div>
</Portal>
{#key anchor}
<Portal {target}>
<div
tabindex="0"
use:positionDropdown={{
anchor,
align,
maxWidth,
useAnchorWidth,
offset,
}}
use:clickOutside={{
callback: dismissible ? handleOutsideClick : () => {},
anchor,
}}
on:keydown={handleEscape}
class="spectrum-Popover is-open"
role="presentation"
transition:fly|local={{ y: -20, duration: 200 }}
>
<slot />
</div>
</Portal>
{/key}
{/if}
<style>
.spectrum-Popover {
min-width: var(--spectrum-global-dimension-size-2000);
border-color: var(--spectrum-global-color-gray-300);
}
.spectrum-Popover.is-open.spectrum-Popover--withTip {
margin-top: var(--spacing-xs);
margin-left: var(--spacing-xl);
}
:global(.spectrum-Popover--bottom .spectrum-Popover-tip),
:global(.spectrum-Popover--top .spectrum-Popover-tip) {
left: 90%;
margin-left: calc(var(--spectrum-global-dimension-size-150) * -1);
overflow: auto;
}
</style>

View File

@ -8,7 +8,6 @@
export let icon = ""
export let selected = false
export let disabled = false
export let dataCy
export let badge = ""
</script>
@ -17,7 +16,6 @@
class:is-selected={selected}
class:is-disabled={disabled}
on:click
data-cy={dataCy}
>
{#if heading}
<h2 class="spectrum-SideNav-heading" id="nav-heading-{heading}">

View File

@ -3,6 +3,7 @@
import Portal from "svelte-portal"
export let title
export let icon = ""
export let id
const dispatch = createEventDispatcher()
let selected = getContext("tab")
@ -31,10 +32,7 @@
$: {
if ($selected.title === title && tab_internal) {
if ($selected.info?.left !== tab_internal.getBoundingClientRect().left) {
$selected = {
...$selected,
info: tab_internal.getBoundingClientRect(),
}
setTabInfo()
}
}
}
@ -50,6 +48,7 @@
</script>
<div
{id}
bind:this={tab_internal}
on:click={onClick}
class:is-selected={$selected.title === title}

View File

@ -1,3 +1,4 @@
export default {
Modal: "bbui-modal",
PopoverRoot: "bbui-popover-root",
}

View File

@ -27,6 +27,7 @@ export { default as RadioGroup } from "./Form/RadioGroup.svelte"
export { default as Checkbox } from "./Form/Checkbox.svelte"
export { default as InputDropdown } from "./Form/InputDropdown.svelte"
export { default as PickerDropdown } from "./Form/PickerDropdown.svelte"
export { default as EnvDropdown } from "./Form/EnvDropdown.svelte"
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
export { default as Popover } from "./Popover/Popover.svelte"
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
@ -75,6 +76,7 @@ export { default as ListItem } from "./List/ListItem.svelte"
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
export { default as Slider } from "./Form/Slider.svelte"
export { default as Accordion } from "./Accordion/Accordion.svelte"
// Renderers
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
@ -101,3 +103,6 @@ export { banner, BANNER_TYPES } from "./Stores/banner"
// Helpers
export * as Helpers from "./helpers"
// Fancy form components
export * from "./FancyForm"

View File

@ -109,6 +109,11 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@spectrum-css/accordion@3.0.24":
version "3.0.24"
resolved "https://registry.yarnpkg.com/@spectrum-css/accordion/-/accordion-3.0.24.tgz#f89066c120c57b0cfc9aba66d60c39fc1cf69f74"
integrity sha512-jNOmUsxmiT3lRLButnN5KKHM94fd+87fjiF8L0c4uRNgJl6ZsBuxPXrM15lV4y1f8D2IACAw01/ZkGRAeaCOFA==
"@spectrum-css/actionbutton@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@spectrum-css/actionbutton/-/actionbutton-1.0.1.tgz#9c75da37ea6915919fb574c74bd60dacc03b6577"

View File

@ -1,19 +0,0 @@
{
"baseUrl": "http://localhost:4100",
"video": true,
"projectId": "bmbemn",
"reporter": "cypress-multi-reporters",
"reporterOptions": {
"configFile": "reporterConfig.json"
},
"env": {
"PORT": "4100",
"WORKER_PORT": "4200",
"JWT_SECRET": "test",
"HOST_IP": ""
},
"retries": {
"runMode": 1,
"openMode": 0
}
}

View File

@ -1,3 +0,0 @@
{
"budibase": "CB373643-3FC4-4902-9E31-449C0ED066B6"
}

View File

@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

File diff suppressed because one or more lines are too long

View File

@ -1,5 +0,0 @@
{
"id": 8739,
"name": "Jane",
"email": "jane@example.com"
}

View File

@ -1,232 +0,0 @@
[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "Shanna@melissa.tv",
"address": {
"street": "Victor Plains",
"suite": "Suite 879",
"city": "Wisokyburgh",
"zipcode": "90566-7771",
"geo": {
"lat": "-43.9509",
"lng": "-34.4618"
}
},
"phone": "010-692-6593 x09125",
"website": "anastasia.net",
"company": {
"name": "Deckow-Crist",
"catchPhrase": "Proactive didactic contingency",
"bs": "synergize scalable supply-chains"
}
},
{
"id": 3,
"name": "Clementine Bauch",
"username": "Samantha",
"email": "Nathan@yesenia.net",
"address": {
"street": "Douglas Extension",
"suite": "Suite 847",
"city": "McKenziehaven",
"zipcode": "59590-4157",
"geo": {
"lat": "-68.6102",
"lng": "-47.0653"
}
},
"phone": "1-463-123-4447",
"website": "ramiro.info",
"company": {
"name": "Romaguera-Jacobson",
"catchPhrase": "Face to face bifurcated interface",
"bs": "e-enable strategic applications"
}
},
{
"id": 4,
"name": "Patricia Lebsack",
"username": "Karianne",
"email": "Julianne.OConner@kory.org",
"address": {
"street": "Hoeger Mall",
"suite": "Apt. 692",
"city": "South Elvis",
"zipcode": "53919-4257",
"geo": {
"lat": "29.4572",
"lng": "-164.2990"
}
},
"phone": "493-170-9623 x156",
"website": "kale.biz",
"company": {
"name": "Robel-Corkery",
"catchPhrase": "Multi-tiered zero tolerance productivity",
"bs": "transition cutting-edge web services"
}
},
{
"id": 5,
"name": "Chelsey Dietrich",
"username": "Kamren",
"email": "Lucio_Hettinger@annie.ca",
"address": {
"street": "Skiles Walks",
"suite": "Suite 351",
"city": "Roscoeview",
"zipcode": "33263",
"geo": {
"lat": "-31.8129",
"lng": "62.5342"
}
},
"phone": "(254)954-1289",
"website": "demarco.info",
"company": {
"name": "Keebler LLC",
"catchPhrase": "User-centric fault-tolerant solution",
"bs": "revolutionize end-to-end systems"
}
},
{
"id": 6,
"name": "Mrs. Dennis Schulist",
"username": "Leopoldo_Corkery",
"email": "Karley_Dach@jasper.info",
"address": {
"street": "Norberto Crossing",
"suite": "Apt. 950",
"city": "South Christy",
"zipcode": "23505-1337",
"geo": {
"lat": "-71.4197",
"lng": "71.7478"
}
},
"phone": "1-477-935-8478 x6430",
"website": "ola.org",
"company": {
"name": "Considine-Lockman",
"catchPhrase": "Synchronised bottom-line interface",
"bs": "e-enable innovative applications"
}
},
{
"id": 7,
"name": "Kurtis Weissnat",
"username": "Elwyn.Skiles",
"email": "Telly.Hoeger@billy.biz",
"address": {
"street": "Rex Trail",
"suite": "Suite 280",
"city": "Howemouth",
"zipcode": "58804-1099",
"geo": {
"lat": "24.8918",
"lng": "21.8984"
}
},
"phone": "210.067.6132",
"website": "elvis.io",
"company": {
"name": "Johns Group",
"catchPhrase": "Configurable multimedia task-force",
"bs": "generate enterprise e-tailers"
}
},
{
"id": 8,
"name": "Nicholas Runolfsdottir V",
"username": "Maxime_Nienow",
"email": "Sherwood@rosamond.me",
"address": {
"street": "Ellsworth Summit",
"suite": "Suite 729",
"city": "Aliyaview",
"zipcode": "45169",
"geo": {
"lat": "-14.3990",
"lng": "-120.7677"
}
},
"phone": "586.493.6943 x140",
"website": "jacynthe.com",
"company": {
"name": "Abernathy Group",
"catchPhrase": "Implemented secondary concept",
"bs": "e-enable extensible e-tailers"
}
},
{
"id": 9,
"name": "Glenna Reichert",
"username": "Delphine",
"email": "Chaim_McDermott@dana.io",
"address": {
"street": "Dayna Park",
"suite": "Suite 449",
"city": "Bartholomebury",
"zipcode": "76495-3109",
"geo": {
"lat": "24.6463",
"lng": "-168.8889"
}
},
"phone": "(775)976-6794 x41206",
"website": "conrad.com",
"company": {
"name": "Yost and Sons",
"catchPhrase": "Switchable contextually-based project",
"bs": "aggregate real-time technologies"
}
},
{
"id": 10,
"name": "Clementina DuBuque",
"username": "Moriah.Stanton",
"email": "Rey.Padberg@karina.biz",
"address": {
"street": "Kattie Turnpike",
"suite": "Suite 198",
"city": "Lebsackbury",
"zipcode": "31428-2261",
"geo": {
"lat": "-38.2386",
"lng": "57.2232"
}
},
"phone": "024-648-3804",
"website": "ambrose.net",
"company": {
"name": "Hoeger LLC",
"catchPhrase": "Centralized empowering task-force",
"bs": "target end-to-end tables"
}
}
]

View File

@ -1,45 +0,0 @@
import filterTests from "../support/filterTests"
const interact = require('../support/interact')
filterTests(['all'], () => {
xcontext("Add Multi-Option Datatype", () => {
before(() => {
cy.login()
cy.createTestApp()
})
it("should create a new table, with data", () => {
cy.createTable("Multi Data")
cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
cy.addRowMultiValue(["1", "2", "3", "4", "5"])
})
it("should add form with multi select picker, containing 5 options", () => {
cy.navigateToFrontend()
// Add data provider
cy.searchAndAddComponent("Data Provider")
cy.get(interact.DATASOURCE_PROP_CONTROL).click()
cy.get(interact.DROPDOWN).contains("Multi Data").click()
// Add Form with schema to match table
cy.searchAndAddComponent("Form")
cy.get(interact.DATASOURCE_PROP_CONTROL).click()
cy.get(interact.DROPDOWN).contains("Multi Data").click()
// Add multi-select picker to form
cy.searchAndAddComponent("Multi-select Picker").then(componentId => {
cy.get(interact.DATASOURCE_FIELD_CONTROL).type("Test Data").type("{enter}")
cy.wait(1000)
cy.getComponent(componentId).contains("Choose some options").click()
// Check picker has 5 items
cy.getComponent(componentId).find("li").should("have.length", 5)
// Select all items
for (let i = 1; i < 6; i++) {
cy.getComponent(componentId).find("li").contains(i).click()
}
// Check items have been selected
cy.getComponent(componentId)
.find(interact.SPECTRUM_PICKER_LABEL)
.contains("(5)")
})
})
})
})

View File

@ -1,43 +0,0 @@
import filterTests from "../support/filterTests"
const interact = require('../support/interact')
filterTests(['all'], () => {
xcontext("Add Radio Buttons", () => {
before(() => {
cy.login()
cy.createTestApp()
})
it("should add Radio Buttons options picker on form, add data, and confirm", () => {
cy.navigateToFrontend()
cy.searchAndAddComponent("Form")
cy.searchAndAddComponent("Options Picker").then((componentId) => {
// Provide field setting
cy.get(interact.DATASOURCE_FIELD_CONTROL).type("1")
// Open dropdown and select Radio buttons
cy.get(interact.OPTION_TYPE_PROP_CONTROL).click().then(() => {
cy.get(interact.SPECTRUM_POPOVER).contains('Radio buttons')
.click()
})
const radioButtonsTotal = 3
// Add values and confirm total
addRadioButtonData(radioButtonsTotal)
cy.getComponent(componentId).find('[type="radio"]')
.should('have.length', radioButtonsTotal)
})
})
const addRadioButtonData = (totalRadioButtons) => {
cy.get(interact.OPTION_SOURCE_PROP_CONROL).click().then(() => {
cy.get(interact.SPECTRUM_POPOVER).contains('Custom')
.click()
.wait(1000)
})
cy.addCustomSourceOptions(totalRadioButtons)
}
after(() => {
cy.deleteAllApps()
})
})
})

View File

@ -1,116 +0,0 @@
import filterTests from "../../support/filterTests"
const interact = require('../../support/interact')
filterTests(["smoke", "all"], () => {
xcontext("Account Portals", () => {
const bbUserEmail = "bbuser@test.com"
before(() => {
cy.login()
cy.deleteApp("Cypress Tests")
cy.createApp("Cypress Tests", false)
// Create new user
cy.wait(500)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.createUser(bbUserEmail)
cy.contains("bbuser").click()
cy.wait(500)
// Reset password
cy.get(".title").within(() => {
cy.get(interact.SPECTRUM_ICON).click({ force: true })
})
cy.get(interact.SPECTRUM_MENU).within(() => {
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force password reset").click({ force: true })
})
cy.get(interact.SPECTRUM_DIALOG_GRID)
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd')
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true })
// Login as new user and set password
cy.logOut()
cy.get('@pwd').then((pwd) => {
cy.login(bbUserEmail, pwd)
})
for (let i = 0; i < 2; i++) {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test")
}
cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true })
//cy.logoutNoAppGrid()
})
xit("should verify Standard Portal", () => {
// Development access should be disabled (Admin access is already disabled)
cy.login()
cy.setUserRole("bbuser", "App User")
bbUserLogin()
// Verify Standard Portal
cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections
cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button
cy.get(".app").should('not.exist') // No apps -> no roles assigned to user
cy.get(interact.CONTAINER).should('contain', bbUserEmail) // Message containing users email
cy.logoutNoAppGrid()
})
xit("should verify Admin Portal", () => {
cy.login()
// Configure user role
cy.setUserRole("bbuser", "Admin")
bbUserLogin()
// Verify available options for Admin portal
cy.get(interact.SPECTRUM_SIDENAV)
.should('contain', 'Apps')
//.and('contain', 'Usage')
.and('contain', 'Users')
.and('contain', 'Auth')
.and('contain', 'Email')
.and('contain', 'Organisation')
.and('contain', 'Theming')
.and('contain', 'Update')
//.and('contain', 'Upgrade')
cy.logOut()
})
xit("should verify Development Portal", () => {
// Only Development access should be enabled
cy.login()
cy.setUserRole("bbuser", "Developer")
bbUserLogin()
// Verify available options for Admin portal
cy.get(interact.SPECTRUM_SIDENAV)
.should('contain', 'Apps')
//.and('contain', 'Usage')
.and('not.contain', 'Users')
.and('not.contain', 'Auth')
.and('not.contain', 'Email')
.and('not.contain', 'Organisation')
.and('contain', 'Theming')
.and('not.contain', 'Update')
.and('not.contain', 'Upgrade')
cy.logOut()
})
const bbUserLogin = () => {
// Login as bbuser
cy.logOut()
cy.login(bbUserEmail, "test")
}
after(() => {
cy.login()
// Delete BB user
cy.deleteUser(bbUserEmail)
})
})
})

View File

@ -1,178 +0,0 @@
import filterTests from "../../support/filterTests"
// const interact = require("../support/interact")
filterTests(["smoke", "all"], () => {
context("Auth Configuration", () => {
before(() => {
cy.login()
})
after(() => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.location().should(loc => {
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
})
cy.get("[data-cy=new-scope-input]").clear()
cy.get("div.content").scrollTo("bottom")
cy.get("[data-cy=oidc-active]").click()
cy.get("[data-cy=oidc-active]").should('not.be.checked')
cy.intercept("POST", "/api/global/configs").as("updateAuth")
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
cy.wait("@updateAuth")
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
cy.get(".spectrum-Toast-content")
.contains("Settings saved")
.should("be.visible")
})
it("Should allow updating of the OIDC config", () => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.location().should(loc => {
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
})
cy.get("div.content").scrollTo("bottom")
cy.get(".spectrum-Toast .spectrum-ClearButton").click()
cy.get("input[data-cy=configUrl]").type("http://budi-auth.com/v2")
cy.get("input[data-cy=clientID]").type("34ac6a13-f24a-4b52-c70d-fa544ffd11b2")
cy.get("input[data-cy=clientSecret]").type("12A8Q~4nS_DWhOOJ2vWIRsNyDVsdtXPD.Zxa9df_")
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
cy.intercept("POST", "/api/global/configs").as("updateAuth")
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
cy.wait("@updateAuth")
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
cy.get(".spectrum-Toast-content")
.contains("Settings saved")
.should("be.visible")
})
it("Should display default scopes in advanced config.", () => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.location().should(loc => {
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
})
cy.get("div.content").scrollTo("bottom")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get(".spectrum-Tags-item").contains("openid")
cy.get(".spectrum-Tags-item").contains("openid").find(".spectrum-ClearButton").should("not.exist")
cy.get(".spectrum-Tags-item").contains("offline_access")
cy.get(".spectrum-Tags-item").contains("email")
cy.get(".spectrum-Tags-item").contains("profile")
})
it("Add a new scopes", () => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.location().should(loc => {
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
})
cy.get("div.content").scrollTo("bottom")
cy.get("[data-cy=new-scope-input]").type("Sample{enter}")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 5)
cy.get(".spectrum-Tags-item").contains("Sample")
cy.get(".auth-form input.spectrum-Textfield-input").type("Another ")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 6)
cy.get(".spectrum-Tags-item").contains("Another")
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
cy.intercept("POST", "/api/global/configs").as("updateAuth")
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
cy.wait("@updateAuth")
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
cy.reload()
cy.get("div.content").scrollTo("bottom")
cy.get(".spectrum-Tags-item").contains("openid")
cy.get(".spectrum-Tags-item").contains("offline_access")
cy.get(".spectrum-Tags-item").contains("email")
cy.get(".spectrum-Tags-item").contains("profile")
cy.get(".spectrum-Tags-item").contains("Sample")
cy.get(".spectrum-Tags-item").contains("Another")
})
it("Should allow the removal of auth scopes", () => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.location().should(loc => {
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
})
cy.get("div.content").scrollTo("bottom")
cy.get(".spectrum-Tags-item").contains("offline_access").parent().find(".spectrum-ClearButton").click()
cy.get(".spectrum-Tags-item").contains("profile").parent().find(".spectrum-ClearButton").click()
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist")
cy.get(".spectrum-Tags-item").contains("profile").should("not.exist")
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
cy.intercept("POST", "/api/global/configs").as("updateAuth")
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
cy.wait("@updateAuth")
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
cy.get(".spectrum-Toast-content")
.contains("Settings saved")
.should("be.visible")
cy.reload()
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist")
cy.get(".spectrum-Tags-item").contains("profile").should("not.exist")
})
it("Should allow auth scopes to be reset to the core defaults.", () => {
cy.get(".spectrum-SideNav li").contains("Auth").click()
cy.get("div.content").scrollTo("bottom")
cy.get("[data-cy=restore-oidc-default-scopes]").click({force: true})
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get(".spectrum-Tags-item").contains("openid")
cy.get(".spectrum-Tags-item").contains("offline_access")
cy.get(".spectrum-Tags-item").contains("email")
cy.get(".spectrum-Tags-item").contains("profile")
})
it("Should not allow invalid characters in the auth scopes", () => {
cy.get("[data-cy=new-scope-input]").type("thisIsInvalid\\{enter}")
cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get("[data-cy=new-scope-input]").clear()
cy.get("[data-cy=new-scope-input]").type("alsoInvalid\"{enter}")
cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
cy.get("[data-cy=new-scope-input]").clear()
})
it("Should not allow duplicate auth scopes", () => {
cy.get("[data-cy=new-scope-input]").type("offline_access{enter}")
cy.get(".spectrum-Form-itemField .error").contains("Auth scope already exists")
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
})
})
})

View File

@ -1,238 +0,0 @@
import filterTests from "../../support/filterTests"
const interact = require('../../support/interact')
filterTests(["smoke", "all"], () => {
xcontext("User Management", () => {
before(() => {
cy.login()
cy.deleteApp("Cypress Tests")
cy.createApp("Cypress Tests", false)
})
xit("should create a user via basic onboarding", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.createUser("bbuser@test.com")
cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser")
})
xit("should confirm App User role for a New User", () => {
cy.contains("bbuser").click()
cy.get(".spectrum-Form-itemField").eq(3).should('contain', 'App User')
// User should not have app access
cy.get(".spectrum-Heading").contains("Apps").parent().within(() => {
cy.get(interact.LIST_ITEMS, { timeout: 500 }).should("contain", "This user has access to no apps")
})
})
if (Cypress.env("TEST_ENV")) {
xit("should assign role types", () => {
// 3 apps minimum required - to assign an app to each role type
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length < 3) {
for (let i = 1; i < 3; i++) {
const uuid = () => Cypress._.random(0, 1e6)
const name = uuid()
if(i < 1){
cy.createApp(name, false)
} else {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.wait(1000)
cy.get(interact.CREATE_APP_BUTTON, { timeout: 2000 }).click({ force: true })
cy.createAppFromScratch(name)
}
}
}
})
// Navigate back to the user
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click()
cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).contains("bbuser").click()
cy.get(interact.SPECTRUM_HEADING).contains("bbuser", { timeout: 2000})
for (let i = 0; i < 3; i++) {
cy.get(interact.SPECTRUM_TABLE, { timeout: 3000})
.eq(1)
.find(interact.SPECTRUM_TABLE_ROW)
.eq(0)
.find(interact.SPECTRUM_TABLE_CELL)
.eq(0)
.click()
cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 })
.contains("Choose an option")
.click()
.then(() => {
if (i == 0) {
cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Admin").click({ force: true })
}
else if (i == 1) {
cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Power").click({ force: true })
}
else if (i == 2) {
cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Basic").click({ force: true })
}
cy.get(interact.SPECTRUM_BUTTON, { timeout: 2000 })
.contains("Update role")
.click({ force: true })
})
cy.reload()
cy.wait(1000)
}
// Confirm roles exist within Configure roles table
cy.get(interact.SPECTRUM_TABLE, { timeout: 20000 })
.eq(0)
.within(assginedRoles => {
expect(assginedRoles).to.contain("Admin")
expect(assginedRoles).to.contain("Power")
expect(assginedRoles).to.contain("Basic")
})
})
xit("should unassign role types", () => {
// Set each app within Configure roles table to 'No Access'
cy.get(interact.SPECTRUM_TABLE)
.eq(0)
.find(interact.SPECTRUM_TABLE_ROW)
.its("length")
.then(len => {
for (let i = 0; i < len; i++) {
cy.get(interact.SPECTRUM_TABLE)
.eq(0)
.find(interact.SPECTRUM_TABLE_ROW)
.eq(0)
.find(interact.SPECTRUM_TABLE_CELL)
.eq(0)
.click()
.then(() => {
cy.get(interact.SPECTRUM_PICKER).eq(1).click({ force: true })
cy.get(interact.SPECTRUM_POPOVER, { timeout: 500 }).contains("No Access").click()
})
cy.get(interact.SPECTRUM_BUTTON)
.contains("Update role")
.click({ force: true })
}
})
// Confirm Configure roles table no longer has any apps in it
cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).eq(0).contains("No rows found")
})
}
xit("should enable Developer access and verify application access", () => {
// Enable Developer access
cy.get(interact.FIELD)
.eq(4)
.within(() => {
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true })
})
// No Access table should now be empty
cy.get(interact.CONTAINER)
.contains("No Access")
.parent()
.within(() => {
cy.get(interact.SPECTRUM_TABLE).contains("No rows found")
})
// Each app within Configure roles should have Admin access
cy.get(interact.SPECTRUM_TABLE)
.eq(0)
.find(interact.SPECTRUM_TABLE_ROW)
.its("length")
.then(len => {
for (let i = 0; i < len; i++) {
cy.get(interact.SPECTRUM_TABLE)
.eq(0)
.find(interact.SPECTRUM_TABLE_ROW)
.eq(i)
.contains("Admin")
cy.wait(500)
}
})
})
xit("should disable Developer access and verify application access", () => {
// Disable Developer access
cy.get(interact.FIELD)
.eq(4)
.within(() => {
cy.get(".spectrum-Switch-input").click({ force: true })
})
// Configure roles table should now be empty
cy.get(interact.CONTAINER)
.contains("Configure roles")
.parent()
.within(() => {
cy.get(interact.SPECTRUM_TABLE).contains("No rows found")
})
})
xit("Should edit user details within user details page", () => {
// Add First name
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
cy.wait(500)
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb")
})
// Add Last name
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
cy.wait(500)
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test")
})
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).click()
// Reload page
cy.reload()
// Confirm details have been saved
cy.get(interact.FIELD, { timeout: 20000 }).eq(1).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb")
})
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test")
})
})
xit("should reset the users password", () => {
cy.get(".title").within(() => {
cy.get(interact.SPECTRUM_ICON).click({ force: true })
})
cy.get(interact.SPECTRUM_MENU).within(() => {
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force password reset").click({ force: true })
})
// Reset password modal
cy.get(interact.SPECTRUM_DIALOG_GRID)
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd')
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true })
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").should('not.exist')
// Logout, then login with new password
cy.logOut()
cy.get('@pwd').then((pwd) => {
cy.login("bbuser@test.com", pwd)
})
// Reset password screen
for (let i = 0; i < 2; i++) {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test")
}
cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true })
// Confirm user logged in afer password change
cy.login("bbuser@test.com", "test")
cy.get(".avatar > .icon").click({ force: true })
cy.get(".spectrum-Menu-item").contains("Update user information").click({ force: true })
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT)
.eq(0)
.invoke('val').should('eq', 'bbuser@test.com')
// Logout and login as previous user
cy.logoutNoAppGrid()
cy.login()
})
xit("should delete a user", () => {
cy.deleteUser("bbuser@test.com")
cy.get(interact.SPECTRUM_TABLE, { timeout: 4000 }).should("not.have.text", "bbuser")
})
})
})

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