Merge branch 'develop' into feature/licensing
This commit is contained in:
commit
715d42d3e6
|
@ -4,4 +4,6 @@ dist
|
||||||
packages/server/builder
|
packages/server/builder
|
||||||
packages/server/coverage
|
packages/server/coverage
|
||||||
packages/server/client
|
packages/server/client
|
||||||
packages/builder/.routify
|
packages/builder/.routify
|
||||||
|
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||||
|
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
|
@ -43,4 +43,8 @@ jobs:
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
# TODO: parallelise this
|
# TODO: parallelise this
|
||||||
- run: yarn test:e2e:ci
|
- name: Cypress run
|
||||||
|
uses: cypress-io/github-action@v2
|
||||||
|
with:
|
||||||
|
install: false
|
||||||
|
command: yarn test:e2e:ci
|
||||||
|
|
|
@ -38,6 +38,17 @@ jobs:
|
||||||
fi
|
fi
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Tag and release Proxy service docker image
|
||||||
|
run: |
|
||||||
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
|
yarn build:docker:proxy:prod
|
||||||
|
docker tag proxy-service budibase/proxy:$PROD_TAG
|
||||||
|
docker push budibase/proxy:$PROD_TAG
|
||||||
|
env:
|
||||||
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||||
|
PROD_TAG: k8s
|
||||||
|
|
||||||
- name: Configure AWS Credentials
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -23,12 +23,24 @@ jobs:
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
aws-region: eu-west-1
|
aws-region: eu-west-1
|
||||||
|
|
||||||
|
|
||||||
- name: Get the latest budibase release version
|
- name: Get the latest budibase release version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Tag and release Proxy service docker image
|
||||||
|
run: |
|
||||||
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
|
yarn build:docker:proxy:preprod
|
||||||
|
docker tag proxy-service budibase/proxy:$PREPROD_TAG
|
||||||
|
docker push budibase/proxy:$PREPROD_TAG
|
||||||
|
env:
|
||||||
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||||
|
PREPROD_TAG: k8s-preprod
|
||||||
|
|
||||||
- name: Pull values.yaml from budibase-infra
|
- name: Pull values.yaml from budibase-infra
|
||||||
run: |
|
run: |
|
||||||
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
||||||
|
|
|
@ -47,6 +47,13 @@ jobs:
|
||||||
yarn
|
yarn
|
||||||
yarn build
|
yarn build
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
- name: Build OpenAPI spec
|
||||||
|
run: |
|
||||||
|
pushd packages/server
|
||||||
|
yarn
|
||||||
|
yarn specs
|
||||||
|
popd
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@v1
|
uses: azure/setup-helm@v1
|
||||||
|
@ -56,6 +63,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
git config user.name "Budibase Helm Bot"
|
git config user.name "Budibase Helm Bot"
|
||||||
git config user.email "<>"
|
git config user.email "<>"
|
||||||
|
git reset --hard
|
||||||
git pull
|
git pull
|
||||||
helm package charts/budibase
|
helm package charts/budibase
|
||||||
git checkout gh-pages
|
git checkout gh-pages
|
||||||
|
@ -77,3 +85,5 @@ jobs:
|
||||||
packages/cli/build/cli-win.exe
|
packages/cli/build/cli-win.exe
|
||||||
packages/cli/build/cli-linux
|
packages/cli/build/cli-linux
|
||||||
packages/cli/build/cli-macos
|
packages/cli/build/cli-macos
|
||||||
|
packages/server/specs/openapi.yaml
|
||||||
|
packages/server/specs/openapi.json
|
||||||
|
|
|
@ -96,3 +96,6 @@ hosting/proxy/.generated-nginx.prod.conf
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
hosting/.generated*
|
||||||
|
packages/builder/cypress.env.json
|
||||||
|
stats.html
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
node_modules
|
node_modules
|
||||||
public
|
|
||||||
dist
|
dist
|
||||||
*.spec.js
|
*.spec.js
|
||||||
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
|
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
|
||||||
packages/server/builder
|
packages/server/builder
|
||||||
packages/server/coverage
|
packages/server/coverage
|
||||||
packages/server/client
|
packages/server/client
|
||||||
packages/builder/.routify
|
packages/server/src/definitions/openapi.ts
|
||||||
|
packages/builder/.routify
|
||||||
|
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||||
|
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||||
|
|
35
README.md
35
README.md
|
@ -11,7 +11,7 @@
|
||||||
The low code platform you'll enjoy using
|
The low code platform you'll enjoy using
|
||||||
</h3>
|
</h3>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Budibase is an open source low-code platform, and the easiest way to build internal tools that improve productivity.
|
Budibase is an open source low-code platform, and the easiest way to build internal apps that improve productivity.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
|
@ -40,9 +40,11 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
<a href="https://docs.budibase.com/getting-started">Get started</a>
|
<a href="https://account.budibase.app/register">Get started - we host (Budibase Cloud)</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a href="https://docs.budibase.com">Docs</a>
|
<a href="https://docs.budibase.com/docs/hosting-methods">Get started - you host (Docker, K8s, DO)</a>
|
||||||
|
<span> · </span>
|
||||||
|
<a href="https://docs.budibase.com/docs">Docs</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
|
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
|
||||||
<span> · </span>
|
<span> · </span>
|
||||||
|
@ -100,16 +102,37 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
|
||||||
|
|
||||||
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
||||||
|
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
|
### Extend Budibase with its Public API
|
||||||
|
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
|
||||||
|
|
||||||
|
- Budibase as a backend
|
||||||
|
- Inter-operability
|
||||||
|
|
||||||
|
Guide: [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
|
||||||
|
|
||||||
|
#### Docs
|
||||||
|
You can learn more about the Budibase API at the following places:
|
||||||
|
|
||||||
|
- [General documentation](https://docs.budibase.com/docs/public-api) : Learn how to get your API key, how to use spec, and how to use with Postman
|
||||||
|
- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1647858558/Feb%20release/Start_building_with_Budibase_s_API_3_rhlzhv.png">
|
||||||
|
</p>
|
||||||
|
<br /><br />
|
||||||
|
|
||||||
<br /><br /><br />
|
<br /><br /><br />
|
||||||
|
|
||||||
## 🏁 Get started
|
## 🏁 Get started
|
||||||
|
|
||||||
<a href="https://docs.budibase.com/self-hosting/self-host"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
|
<a href="https://docs.budibase.com/docs/hosting-methods"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
|
||||||
|
|
||||||
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
||||||
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
||||||
|
|
||||||
### [Get started with self-hosting Budibase](https://docs.budibase.com/self-hosting/self-host)
|
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
|
||||||
|
|
||||||
### [Get started with Budibase Cloud](https://budibase.com)
|
### [Get started with Budibase Cloud](https://budibase.com)
|
||||||
|
|
||||||
|
@ -118,7 +141,7 @@ Or use Budibase Cloud if you don't need to self-host, and would like to get star
|
||||||
|
|
||||||
## 🎓 Learning Budibase
|
## 🎓 Learning Budibase
|
||||||
|
|
||||||
The Budibase documentation [lives here](https://docs.budibase.com).
|
The Budibase documentation [lives here](https://docs.budibase.com/docs).
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Budibase API + Next.js example
|
||||||
|
|
||||||
|
This is an example of how Budibase can be used as a backend for a Postgres database for a Next.js sales app. You will
|
||||||
|
need to follow the walk-through that has been published in the Budibase docs to set up your Budibase app for this example.
|
||||||
|
|
||||||
|
## Pre-requisites
|
||||||
|
|
||||||
|
To use this example you will need:
|
||||||
|
1. [Docker](https://www.docker.com/)
|
||||||
|
2. [Docker Compose](https://docs.docker.com/compose/)
|
||||||
|
3. [Node.js](https://nodejs.org/en/)
|
||||||
|
4. A self-hosted Budibase installation
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
The first step is to set up the database - you can do this by going to the `db/` directory and running the command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
The next step is to follow the example walk-through and set up a Budibase app as it describes. Once you've done
|
||||||
|
this you can configure the settings in `next.config.js`, specifically the `apiKey`, `host` and `appName`.
|
||||||
|
|
||||||
|
Finally, you can start the dev server with the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessing the app
|
||||||
|
|
||||||
|
Open [http://localhost:3001](http://localhost:3001) with your browser to see the sales app.
|
||||||
|
|
||||||
|
Look in the API routes (`pages/api/sales.ts` and `pages/api/salespeople.ts`) to see how this is integrated with Budibase.
|
||||||
|
There is also a utility file where some core functions and types have been defined, in `utilities/index.ts`.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
This example was set up using [Next.js](https://nextjs.org/) and bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
|
@ -0,0 +1,42 @@
|
||||||
|
import Link from "next/link"
|
||||||
|
import Image from "next/image"
|
||||||
|
import { ReactNotifications } from "react-notifications-component"
|
||||||
|
|
||||||
|
function layout(props: any) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||||
|
<div id="navbar" className="navbar-menu">
|
||||||
|
<div className="logo">
|
||||||
|
<Image alt="logo" src="/bb-emblem.svg" width="50" height="50" />
|
||||||
|
</div>
|
||||||
|
<div className="navbar-start">
|
||||||
|
<Link href="/">
|
||||||
|
<a className="navbar-item">
|
||||||
|
List
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href="/save">
|
||||||
|
<a className="navbar-item">
|
||||||
|
New sale
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="navbar-end">
|
||||||
|
<div className="navbar-item">
|
||||||
|
<div className="buttons">
|
||||||
|
<a className="button is-primary" href="https://budibase.readme.io/reference">
|
||||||
|
<strong>API Documentation</strong>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<ReactNotifications />
|
||||||
|
{props.children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default layout
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Store } from "react-notifications-component"
|
||||||
|
|
||||||
|
const notifications = {
|
||||||
|
error: (error: string, title: string) => {
|
||||||
|
Store.addNotification({
|
||||||
|
container: "top-right",
|
||||||
|
type: "danger",
|
||||||
|
message: error,
|
||||||
|
title: title,
|
||||||
|
dismiss: {
|
||||||
|
duration: 10000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
success: (message: string, title: string) => {
|
||||||
|
Store.addNotification({
|
||||||
|
container: "top-right",
|
||||||
|
type: "success",
|
||||||
|
message: message,
|
||||||
|
title: title,
|
||||||
|
dismiss: {
|
||||||
|
duration: 3000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default notifications
|
|
@ -0,0 +1,17 @@
|
||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: postgres
|
||||||
|
image: postgres
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: root
|
||||||
|
POSTGRES_PASSWORD: root
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- pg_data:/var/lib/postgresql/data/
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
volumes:
|
||||||
|
pg_data:
|
|
@ -0,0 +1,21 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS sales_people (
|
||||||
|
person_id SERIAL PRIMARY KEY,
|
||||||
|
name varchar(200) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sales (
|
||||||
|
sale_id SERIAL PRIMARY KEY,
|
||||||
|
sale_name varchar(200) NOT NULL,
|
||||||
|
sold_by INT,
|
||||||
|
CONSTRAINT sold_by_fk
|
||||||
|
FOREIGN KEY(sold_by)
|
||||||
|
REFERENCES sales_people(person_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO sales_people (name)
|
||||||
|
select 'Salesperson ' || id
|
||||||
|
FROM GENERATE_SERIES(1, 50) as id;
|
||||||
|
|
||||||
|
INSERT INTO sales (sale_name, sold_by)
|
||||||
|
select 'Sale ' || id, floor(random() * 50 + 1)::int
|
||||||
|
FROM GENERATE_SERIES(1, 200) as id;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { components } from "./openapi"
|
||||||
|
|
||||||
|
export type App = components["schemas"]["applicationOutput"]["data"]
|
||||||
|
export type Table = components["schemas"]["tableOutput"]["data"]
|
||||||
|
export type TableSearch = components["schemas"]["tableSearch"]
|
||||||
|
export type AppSearch = components["schemas"]["applicationSearch"]
|
||||||
|
export type RowSearch = components["schemas"]["searchOutput"]
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
@ -0,0 +1,16 @@
|
||||||
|
const { join } = require("path")
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
sassOptions: {
|
||||||
|
includePaths: [join(__dirname, "styles")],
|
||||||
|
},
|
||||||
|
serverRuntimeConfig: {
|
||||||
|
apiKey:
|
||||||
|
"bf4d86af933b5ac0af0fdbe4bf7d89ff-f929752a1eeaafb00f4b5e3325097d51a44fe4b39f22ed857923409cc75414b379323a25ebfb4916",
|
||||||
|
appName: "sales",
|
||||||
|
host: "http://localhost:10000",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "nextjs-api-sales",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 3001",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bulma": "^0.9.3",
|
||||||
|
"next": "12.1.0",
|
||||||
|
"node-fetch": "^3.2.2",
|
||||||
|
"node-sass": "^7.0.1",
|
||||||
|
"react": "17.0.2",
|
||||||
|
"react-dom": "17.0.2",
|
||||||
|
"react-notifications-component": "^3.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "17.0.21",
|
||||||
|
"@types/react": "17.0.39",
|
||||||
|
"eslint": "8.10.0",
|
||||||
|
"eslint-config-next": "12.1.0",
|
||||||
|
"typescript": "4.6.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import "../styles/global.sass"
|
||||||
|
import type { AppProps } from "next/app"
|
||||||
|
import Head from "next/head"
|
||||||
|
import Layout from "../components/layout"
|
||||||
|
|
||||||
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Head>
|
||||||
|
<title>BB NextJS Sales Example</title>
|
||||||
|
</Head>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyApp
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { getApp, findTable, makeCall } from "../../utilities"
|
||||||
|
|
||||||
|
async function getSales(req: any) {
|
||||||
|
const { page } = req.query
|
||||||
|
const { _id: appId } = await getApp()
|
||||||
|
const table = await findTable(appId, "sales")
|
||||||
|
return await makeCall("post", `tables/${table._id}/rows/search`, {
|
||||||
|
appId,
|
||||||
|
body: {
|
||||||
|
limit: 10,
|
||||||
|
sort: {
|
||||||
|
type: "string",
|
||||||
|
order: "descending",
|
||||||
|
column: "sale_id",
|
||||||
|
},
|
||||||
|
paginate: true,
|
||||||
|
bookmark: parseInt(page),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSale(req: any) {
|
||||||
|
const { _id: appId } = await getApp()
|
||||||
|
const table = await findTable(appId, "sales")
|
||||||
|
return await makeCall("post", `tables/${table._id}/rows`, {
|
||||||
|
body: req.body,
|
||||||
|
appId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: any, res: any) {
|
||||||
|
let response: any = {}
|
||||||
|
try {
|
||||||
|
if (req.method === "POST") {
|
||||||
|
response = await saveSale(req)
|
||||||
|
} else if (req.method === "GET") {
|
||||||
|
response = await getSales(req)
|
||||||
|
} else {
|
||||||
|
res.status(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.status(200).json(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(400).send(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { getApp, findTable, makeCall } from "../../utilities"
|
||||||
|
|
||||||
|
async function getSalespeople() {
|
||||||
|
const { _id: appId } = await getApp()
|
||||||
|
const table = await findTable(appId, "sales_people")
|
||||||
|
return await makeCall("post", `tables/${table._id}/rows/search`, {
|
||||||
|
appId,
|
||||||
|
body: {
|
||||||
|
sort: {
|
||||||
|
type: "string",
|
||||||
|
order: "ascending",
|
||||||
|
column: "person_id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: any, res: any) {
|
||||||
|
let response: any = {}
|
||||||
|
try {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
response = await getSalespeople()
|
||||||
|
} else {
|
||||||
|
res.status(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.status(200).json(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(400).send(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import type { NextPage } from "next"
|
||||||
|
import styles from "../styles/home.module.css"
|
||||||
|
import { useState, useEffect, useCallback } from "react"
|
||||||
|
import Notifications from "../components/notifications"
|
||||||
|
|
||||||
|
const Home: NextPage = () => {
|
||||||
|
const [sales, setSales] = useState([])
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
|
||||||
|
const getSales = useCallback(async (page: Number = 1) => {
|
||||||
|
let url = "/api/sales"
|
||||||
|
if (page) {
|
||||||
|
url += `?page=${page}`
|
||||||
|
}
|
||||||
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text()
|
||||||
|
Notifications.error(error, "Failed to get sales")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sales = await response.json()
|
||||||
|
// @ts-ignore
|
||||||
|
setCurrentPage(page)
|
||||||
|
return setSales(sales.data)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const goToNextPage = useCallback(async () => {
|
||||||
|
await getSales(currentPage + 1)
|
||||||
|
}, [currentPage, getSales])
|
||||||
|
|
||||||
|
const goToPrevPage = useCallback(async () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
await getSales(currentPage - 1)
|
||||||
|
}
|
||||||
|
}, [currentPage, getSales])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSales().then(() => {
|
||||||
|
setLoaded(true)
|
||||||
|
}).catch(() => {
|
||||||
|
setSales([])
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.tableSection}>
|
||||||
|
<h1 className="subtitle">Sales</h1>
|
||||||
|
<div className={styles.table}>
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Sale ID</th>
|
||||||
|
<th>name</th>
|
||||||
|
<th>Sold by</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sales.map((sale: any) =>
|
||||||
|
<tr key={sale.sale_id}>
|
||||||
|
<th>{sale.sale_id}</th>
|
||||||
|
<th>{sale.sale_name}</th>
|
||||||
|
<th>{sale.sales_person?.map((person: any) => person.primaryDisplay)[0]}</th>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<button className="button" onClick={goToPrevPage}>Prev Page</button>
|
||||||
|
<button className="button" onClick={goToNextPage}>Next Page</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
|
@ -0,0 +1,81 @@
|
||||||
|
import type { NextPage } from "next"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import styles from "../styles/save.module.css"
|
||||||
|
import Notifications from "../components/notifications"
|
||||||
|
|
||||||
|
const Save: NextPage = () => {
|
||||||
|
const [salespeople, setSalespeople] = useState([])
|
||||||
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
|
||||||
|
const saveSale = useCallback(async (event: any) => {
|
||||||
|
event.preventDefault()
|
||||||
|
const sale = {
|
||||||
|
sale_name: event.target.name.value,
|
||||||
|
sales_person: [event.target.soldBy.value],
|
||||||
|
}
|
||||||
|
const response = await fetch("/api/sales", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(sale),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text()
|
||||||
|
Notifications.error(error, "Failed to save sale")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Notifications.success("Sale saved successfully!", "Sale saved")
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getSalespeople = useCallback(async () => {
|
||||||
|
const response: any = await fetch("/api/salespeople")
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await response.text())
|
||||||
|
}
|
||||||
|
const json = await response.json()
|
||||||
|
setSalespeople(json.data)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSalespeople().then(() => {
|
||||||
|
setLoaded(true)
|
||||||
|
}).catch(() => {
|
||||||
|
setSalespeople([])
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<h1 className="subtitle">New sale</h1>
|
||||||
|
<form onSubmit={saveSale}>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Name</label>
|
||||||
|
<div className="control">
|
||||||
|
<input id="name" className="input" type="text" placeholder="Text input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Sold by</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="select">
|
||||||
|
<select id="soldBy">
|
||||||
|
{salespeople.map((person: any) => <option key={person._id} value={person._id}>{person.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
<button className="button is-link">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Save
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#393C44;}
|
||||||
|
.st1{fill:#FFFFFF;}
|
||||||
|
.st2{fill:#4285F4;}
|
||||||
|
</style>
|
||||||
|
<rect x="-152.17" y="-24.17" class="st0" width="96.17" height="96.17"/>
|
||||||
|
<path class="st1" d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79
|
||||||
|
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"/>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||||
|
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||||
|
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
|
||||||
|
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||||
|
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||||
|
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||||
|
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||||
|
C-93.55,28.92-93.46,28.52-93.46,28.11z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58
|
||||||
|
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
|
||||||
|
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
|
||||||
|
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||||
|
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||||
|
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||||
|
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||||
|
C-108.68,28.92-108.6,28.52-108.6,28.11z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path class="st2" d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79
|
||||||
|
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"/>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||||
|
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||||
|
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
|
||||||
|
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||||
|
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||||
|
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||||
|
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||||
|
C34.45,139.92,34.54,139.52,34.54,139.11z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||||
|
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||||
|
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
|
||||||
|
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
|
||||||
|
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
|
||||||
|
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
|
||||||
|
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M44,48H4c-2.21,0-4-1.79-4-4V4c0-2.21,1.79-4,4-4h40c2.21,0,4,1.79,4,4v40C48,46.21,46.21,48,44,48z"/>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
|
||||||
|
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
||||||
|
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
|
||||||
|
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
||||||
|
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
||||||
|
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
||||||
|
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st1" d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
|
||||||
|
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
||||||
|
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
|
||||||
|
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
||||||
|
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
||||||
|
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
||||||
|
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1,26 @@
|
||||||
|
@charset "utf-8"
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css?family=Roboto:400,700')
|
||||||
|
$family-sans-serif: "Roboto", sans-serif
|
||||||
|
|
||||||
|
#__next
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
justify-content: flex-start
|
||||||
|
align-items: stretch
|
||||||
|
height: 100vh
|
||||||
|
--bg-color: #f5f5f5
|
||||||
|
|
||||||
|
.logo
|
||||||
|
padding: 0.75rem
|
||||||
|
|
||||||
|
@import "../node_modules/bulma/bulma.sass"
|
||||||
|
@import "../node_modules/react-notifications-component/dist/theme.css"
|
||||||
|
|
||||||
|
// applied after bulma styles are enabled
|
||||||
|
html
|
||||||
|
overflow-y: auto
|
||||||
|
|
||||||
|
.navbar
|
||||||
|
background-color: var(--bg-color)
|
||||||
|
color: white
|
|
@ -0,0 +1,30 @@
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5rem 2rem 0;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableSection {
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--bg-color);
|
||||||
|
width: 800px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableSection h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: black;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5rem 2rem 0;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection {
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--bg-color);
|
||||||
|
width: 400px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: black;
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { App, AppSearch, Table, TableSearch } from "../definitions"
|
||||||
|
import getConfig from "next/config"
|
||||||
|
|
||||||
|
const { serverRuntimeConfig } = getConfig()
|
||||||
|
const apiKey = serverRuntimeConfig["apiKey"]
|
||||||
|
const appName = serverRuntimeConfig["appName"]
|
||||||
|
const host = serverRuntimeConfig["host"]
|
||||||
|
|
||||||
|
let APP: App | null = null
|
||||||
|
let TABLES: { [key: string]: Table } = {}
|
||||||
|
|
||||||
|
export async function makeCall(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
opts?: { body?: any; appId?: string }
|
||||||
|
): Promise<any> {
|
||||||
|
const fetchOpts: any = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
"x-budibase-api-key": apiKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (opts?.appId) {
|
||||||
|
fetchOpts.headers["x-budibase-app-id"] = opts.appId
|
||||||
|
}
|
||||||
|
if (opts?.body) {
|
||||||
|
fetchOpts.body =
|
||||||
|
typeof opts.body !== "string" ? JSON.stringify(opts.body) : opts.body
|
||||||
|
fetchOpts.headers["Content-Type"] = "application/json"
|
||||||
|
}
|
||||||
|
const finalUrl = `${host}/api/public/v1/${url}`
|
||||||
|
const response = await fetch(finalUrl, fetchOpts)
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
const error = await response.text()
|
||||||
|
console.error("Budibase server error - ", error)
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getApp(): Promise<App> {
|
||||||
|
if (APP) {
|
||||||
|
return APP
|
||||||
|
}
|
||||||
|
const apps: AppSearch = await makeCall("post", "applications/search", {
|
||||||
|
body: {
|
||||||
|
name: appName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const app = apps.data.find((app: App) => app.name === appName)
|
||||||
|
if (!app) {
|
||||||
|
throw new Error(
|
||||||
|
"Could not find app, please make sure app name in config is correct."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
APP = app
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findTable(
|
||||||
|
appId: string,
|
||||||
|
tableName: string
|
||||||
|
): Promise<Table> {
|
||||||
|
if (TABLES[tableName]) {
|
||||||
|
return TABLES[tableName]
|
||||||
|
}
|
||||||
|
const tables: TableSearch = await makeCall("post", "tables/search", {
|
||||||
|
body: {
|
||||||
|
name: tableName,
|
||||||
|
},
|
||||||
|
appId,
|
||||||
|
})
|
||||||
|
const table = tables.data.find((table: Table) => table.name === tableName)
|
||||||
|
if (!table) {
|
||||||
|
throw new Error(
|
||||||
|
"Could not find table, please make sure your app is configured with the Postgres datasource correctly."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TABLES[tableName] = table
|
||||||
|
return table
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -76,6 +76,7 @@ http {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
|
||||||
proxy_connect_timeout 300;
|
proxy_connect_timeout 300;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
@ -91,4 +92,4 @@ http {
|
||||||
gzip off;
|
gzip off;
|
||||||
gzip_comp_level 4;
|
gzip_comp_level 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,13 +19,7 @@ http {
|
||||||
tcp_nodelay on;
|
tcp_nodelay on;
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
types_hash_max_size 2048;
|
types_hash_max_size 2048;
|
||||||
{{#if compose}}
|
resolver {{ resolver }} valid=10s ipv6=off;
|
||||||
resolver 127.0.0.11 ipv6=off;
|
|
||||||
{{/if}}
|
|
||||||
{{#if k8s}}
|
|
||||||
resolver kube-dns.kube-system.svc.cluster.local valid=10s;
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
|
|
||||||
# buffering
|
# buffering
|
||||||
client_body_buffer_size 1K;
|
client_body_buffer_size 1K;
|
||||||
|
@ -55,7 +49,7 @@ http {
|
||||||
add_header X-Frame-Options SAMEORIGIN always;
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
add_header X-Content-Type-Options nosniff always;
|
add_header X-Content-Type-Options nosniff always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' data https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com; frame-src 'self'; img-src http: https: data; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io ; font-src 'self' data https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com; frame-src 'self' https:; img-src http: https: data; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
|
||||||
|
|
||||||
# upstreams
|
# upstreams
|
||||||
set $apps {{ apps }};
|
set $apps {{ apps }};
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
user nginx;
|
|
||||||
error_log /var/log/nginx/error.log debug;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
worker_processes auto;
|
|
||||||
worker_rlimit_nofile 33282;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
charset utf-8;
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
tcp_nodelay on;
|
|
||||||
server_tokens off;
|
|
||||||
types_hash_max_size 2048;
|
|
||||||
|
|
||||||
# buffering
|
|
||||||
client_body_buffer_size 1K;
|
|
||||||
client_header_buffer_size 1k;
|
|
||||||
client_max_body_size 1k;
|
|
||||||
ignore_invalid_headers off;
|
|
||||||
|
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
|
||||||
default "upgrade";
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 10000 default_server;
|
|
||||||
listen [::]:10000 default_server;
|
|
||||||
server_name _;
|
|
||||||
client_max_body_size 1000m;
|
|
||||||
ignore_invalid_headers off;
|
|
||||||
proxy_buffering off;
|
|
||||||
port_in_redirect off;
|
|
||||||
|
|
||||||
# Security Headers
|
|
||||||
add_header X-Frame-Options SAMEORIGIN always;
|
|
||||||
add_header X-Content-Type-Options nosniff always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me; frame-src 'self'; img-src http: https: data:; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
|
|
||||||
|
|
||||||
location /app {
|
|
||||||
proxy_pass http://app-service:4002;
|
|
||||||
rewrite ^/app/(.*)$ /$1 break;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = / {
|
|
||||||
port_in_redirect off;
|
|
||||||
proxy_pass http://app-service:4002;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /v1/update {
|
|
||||||
proxy_pass http://watchtower-service:8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /builder/ {
|
|
||||||
port_in_redirect off;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_pass http://app-service:4002;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/(builder|app_) {
|
|
||||||
port_in_redirect off;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_pass http://app-service:4002;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/api/(system|admin|global)/ {
|
|
||||||
proxy_pass http://worker-service:4003;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /worker/ {
|
|
||||||
proxy_pass http://worker-service:4003;
|
|
||||||
rewrite ^/worker/(.*)$ /$1 break;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
# calls to the API are rate limited with bursting
|
|
||||||
limit_req zone=ratelimit burst=20 nodelay;
|
|
||||||
|
|
||||||
# 120s timeout on API requests
|
|
||||||
proxy_read_timeout 120s;
|
|
||||||
proxy_connect_timeout 120s;
|
|
||||||
proxy_send_timeout 120s;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
|
|
||||||
proxy_pass http://app-service:4002;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /db/ {
|
|
||||||
proxy_pass http://couchdb-service:5984;
|
|
||||||
rewrite ^/db/(.*)$ /$1 break;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
|
|
||||||
proxy_connect_timeout 300;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection "";
|
|
||||||
chunked_transfer_encoding off;
|
|
||||||
proxy_pass http://minio-service:9000;
|
|
||||||
}
|
|
||||||
|
|
||||||
client_header_timeout 60;
|
|
||||||
client_body_timeout 60;
|
|
||||||
keepalive_timeout 60;
|
|
||||||
|
|
||||||
# gzip
|
|
||||||
gzip on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.78",
|
"version": "1.0.91-alpha.6",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
13
package.json
13
package.json
|
@ -40,16 +40,17 @@
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
||||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||||
"lint:fix:eslint": "eslint --fix packages",
|
"lint:fix:eslint": "eslint --fix packages",
|
||||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\"",
|
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
||||||
"lint:fix:ts": "lerna run lint:fix",
|
"lint:fix:ts": "lerna run lint:fix",
|
||||||
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||||
"test:e2e": "lerna run cy:test",
|
"test:e2e": "lerna run cy:test --stream",
|
||||||
"test:e2e:ci": "lerna run cy:ci",
|
"test:e2e:ci": "lerna run cy:ci --stream",
|
||||||
|
"build:specs": "lerna run specs",
|
||||||
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||||
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
||||||
"build:docker:proxy:compose": "lerna run generate:proxy:compose && npm run build:docker:proxy",
|
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
|
||||||
"build:docker:proxy:preprod": "lerna run generate:proxy:preprod && npm run build:docker:proxy",
|
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
|
||||||
"build:docker:proxy:prod": "lerna run generate:proxy:prod && npm run build:docker:proxy",
|
"build:docker:proxy:prod": "node scripts/proxy/generateProxyConfig prod && npm run build:docker:proxy",
|
||||||
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
||||||
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require("./src/security/encryption")
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.78",
|
"version": "1.0.91-alpha.6",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -32,11 +32,10 @@ const populateFromDB = async (userId, tenantId) => {
|
||||||
* @param {*} populateUser function to provide the user for re-caching. default to couch db
|
* @param {*} populateUser function to provide the user for re-caching. default to couch db
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
exports.getUser = async (
|
exports.getUser = async (userId, tenantId = null, populateUser = null) => {
|
||||||
userId,
|
if (!populateUser) {
|
||||||
tenantId = null,
|
populateUser = populateFromDB
|
||||||
populateUser = populateFromDB
|
}
|
||||||
) => {
|
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
try {
|
try {
|
||||||
tenantId = getTenantId()
|
tenantId = getTenantId()
|
||||||
|
|
|
@ -22,3 +22,18 @@ exports.getAccount = async email => {
|
||||||
|
|
||||||
return json[0]
|
return json[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getStatus = async () => {
|
||||||
|
const response = await api.get(`/api/status`, {
|
||||||
|
headers: {
|
||||||
|
[Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Error getting status`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ exports.DocumentTypes = {
|
||||||
APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`,
|
APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`,
|
||||||
ROLE: "role",
|
ROLE: "role",
|
||||||
MIGRATIONS: "migrations",
|
MIGRATIONS: "migrations",
|
||||||
|
DEV_INFO: "devinfo",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.StaticDatabases = {
|
exports.StaticDatabases = {
|
||||||
|
|
|
@ -30,6 +30,7 @@ const UNICODE_MAX = "\ufff0"
|
||||||
|
|
||||||
exports.ViewNames = {
|
exports.ViewNames = {
|
||||||
USER_BY_EMAIL: "by_email",
|
USER_BY_EMAIL: "by_email",
|
||||||
|
BY_API_KEY: "by_api_key",
|
||||||
USER_BY_BUILDERS: "by_builders",
|
USER_BY_BUILDERS: "by_builders",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +69,7 @@ function getDocParams(docType, docId = null, otherProps = {}) {
|
||||||
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
|
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
exports.getDocParams = getDocParams
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new workspace ID.
|
* Generates a new workspace ID.
|
||||||
|
@ -340,6 +342,14 @@ const getConfigParams = ({ type, workspace, user }, otherProps = {}) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new dev info document ID - this is scoped to a user.
|
||||||
|
* @returns {string} The new dev info ID which info for dev (like api key) can be stored under.
|
||||||
|
*/
|
||||||
|
const generateDevInfoID = userId => {
|
||||||
|
return `${DocumentTypes.DEV_INFO}${SEPARATOR}${userId}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
|
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
|
||||||
* @param {Object} db - db instance to query
|
* @param {Object} db - db instance to query
|
||||||
|
@ -430,3 +440,4 @@ exports.getScopedConfig = getScopedConfig
|
||||||
exports.generateConfigID = generateConfigID
|
exports.generateConfigID = generateConfigID
|
||||||
exports.getConfigParams = getConfigParams
|
exports.getConfigParams = getConfigParams
|
||||||
exports.getScopedFullConfig = getScopedFullConfig
|
exports.getScopedFullConfig = getScopedFullConfig
|
||||||
|
exports.generateDevInfoID = generateDevInfoID
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const { DocumentTypes, ViewNames } = require("./utils")
|
const { DocumentTypes, ViewNames } = require("./utils")
|
||||||
|
const { getGlobalDB } = require("../tenancy")
|
||||||
|
|
||||||
function DesignDoc() {
|
function DesignDoc() {
|
||||||
return {
|
return {
|
||||||
|
@ -9,7 +10,8 @@ function DesignDoc() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createUserEmailView = async db => {
|
exports.createUserEmailView = async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
let designDoc
|
let designDoc
|
||||||
try {
|
try {
|
||||||
designDoc = await db.get("_design/database")
|
designDoc = await db.get("_design/database")
|
||||||
|
@ -32,6 +34,28 @@ exports.createUserEmailView = async db => {
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.createApiKeyView = async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
let designDoc
|
||||||
|
try {
|
||||||
|
designDoc = await db.get("_design/database")
|
||||||
|
} catch (err) {
|
||||||
|
designDoc = DesignDoc()
|
||||||
|
}
|
||||||
|
const view = {
|
||||||
|
map: `function(doc) {
|
||||||
|
if (doc._id.startsWith("${DocumentTypes.DEV_INFO}") && doc.apiKey) {
|
||||||
|
emit(doc.apiKey, doc.userId)
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
designDoc.views = {
|
||||||
|
...designDoc.views,
|
||||||
|
[ViewNames.BY_API_KEY]: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
}
|
||||||
|
|
||||||
exports.createUserBuildersView = async db => {
|
exports.createUserBuildersView = async db => {
|
||||||
let designDoc
|
let designDoc
|
||||||
try {
|
try {
|
||||||
|
@ -53,3 +77,29 @@ exports.createUserBuildersView = async db => {
|
||||||
}
|
}
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
|
const CreateFuncByName = {
|
||||||
|
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
|
||||||
|
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
||||||
|
}
|
||||||
|
// can pass DB in if working with something specific
|
||||||
|
if (!db) {
|
||||||
|
db = getGlobalDB()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let response = (await db.query(`database/${viewName}`, params)).rows
|
||||||
|
response = response.map(resp =>
|
||||||
|
params.include_docs ? resp.doc : resp.value
|
||||||
|
)
|
||||||
|
return response.length <= 1 ? response[0] : response
|
||||||
|
} catch (err) {
|
||||||
|
if (err != null && err.name === "not_found") {
|
||||||
|
const createFunc = CreateFuncByName[viewName]
|
||||||
|
await createFunc()
|
||||||
|
return exports.queryGlobalView(viewName, params)
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,9 @@ const { getUser } = require("../cache/user")
|
||||||
const { getSession, updateSessionTTL } = require("../security/sessions")
|
const { getSession, updateSessionTTL } = require("../security/sessions")
|
||||||
const { buildMatcherRegex, matches } = require("./matchers")
|
const { buildMatcherRegex, matches } = require("./matchers")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
const { SEPARATOR, ViewNames, queryGlobalView } = require("../../db")
|
||||||
|
const { getGlobalDB, doInTenant } = require("../tenancy")
|
||||||
|
const { decrypt } = require("../security/encryption")
|
||||||
|
|
||||||
function finalise(
|
function finalise(
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -16,6 +19,33 @@ function finalise(
|
||||||
ctx.version = version
|
ctx.version = version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkApiKey(apiKey, populateUser) {
|
||||||
|
if (apiKey === env.INTERNAL_API_KEY) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
const decrypted = decrypt(apiKey)
|
||||||
|
const tenantId = decrypted.split(SEPARATOR)[0]
|
||||||
|
return doInTenant(tenantId, async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
// api key is encrypted in the database
|
||||||
|
const userId = await queryGlobalView(
|
||||||
|
ViewNames.BY_API_KEY,
|
||||||
|
{
|
||||||
|
key: apiKey,
|
||||||
|
},
|
||||||
|
db
|
||||||
|
)
|
||||||
|
if (userId) {
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
user: await getUser(userId, tenantId, populateUser),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw "Invalid API key"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This middleware is tenancy aware, so that it does not depend on other middlewares being used.
|
* This middleware is tenancy aware, so that it does not depend on other middlewares being used.
|
||||||
* The tenancy modules should not be used here and it should be assumed that the tenancy context
|
* The tenancy modules should not be used here and it should be assumed that the tenancy context
|
||||||
|
@ -79,9 +109,19 @@ module.exports = (
|
||||||
const apiKey = ctx.request.headers[Headers.API_KEY]
|
const apiKey = ctx.request.headers[Headers.API_KEY]
|
||||||
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
||||||
// this is an internal request, no user made it
|
// this is an internal request, no user made it
|
||||||
if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) {
|
if (!authenticated && apiKey) {
|
||||||
authenticated = true
|
const populateUser = opts.populateUser ? opts.populateUser(ctx) : null
|
||||||
internal = true
|
const { valid, user: foundUser } = await checkApiKey(
|
||||||
|
apiKey,
|
||||||
|
populateUser
|
||||||
|
)
|
||||||
|
if (valid && foundUser) {
|
||||||
|
authenticated = true
|
||||||
|
user = foundUser
|
||||||
|
} else if (valid) {
|
||||||
|
authenticated = true
|
||||||
|
internal = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!user && tenantId) {
|
if (!user && tenantId) {
|
||||||
user = { tenantId }
|
user = { tenantId }
|
||||||
|
@ -101,6 +141,7 @@ module.exports = (
|
||||||
// allow configuring for public access
|
// allow configuring for public access
|
||||||
if ((opts && opts.publicAllowed) || publicEndpoint) {
|
if ((opts && opts.publicAllowed) || publicEndpoint) {
|
||||||
finalise(ctx, { authenticated: false, version, publicEndpoint })
|
finalise(ctx, { authenticated: false, version, publicEndpoint })
|
||||||
|
return next()
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(err.status || 403, err)
|
ctx.throw(err.status || 403, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,25 @@ exports.Databases = {
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
|
|
||||||
exports.getRedisOptions = (clustered = false) => {
|
exports.getRedisOptions = (clustered = false) => {
|
||||||
const [host, port, ...rest] = REDIS_URL.split(":")
|
let password = REDIS_PASSWORD
|
||||||
|
let url = REDIS_URL.split("//")
|
||||||
|
// get rid of the protocol
|
||||||
|
url = url.length > 1 ? url[1] : url[0]
|
||||||
|
// check for a password etc
|
||||||
|
url = url.split("@")
|
||||||
|
if (url.length > 1) {
|
||||||
|
// get the password
|
||||||
|
password = url[0].split(":")[1]
|
||||||
|
url = url[1]
|
||||||
|
} else {
|
||||||
|
url = url[0]
|
||||||
|
}
|
||||||
|
const [host, port] = url.split(":")
|
||||||
|
|
||||||
let redisProtocolUrl
|
let redisProtocolUrl
|
||||||
|
|
||||||
// fully qualified redis URL
|
// fully qualified redis URL
|
||||||
if (rest.length && /rediss?/.test(host)) {
|
if (/rediss?:\/\//.test(REDIS_URL)) {
|
||||||
redisProtocolUrl = REDIS_URL
|
redisProtocolUrl = REDIS_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,13 +51,13 @@ exports.getRedisOptions = (clustered = false) => {
|
||||||
if (clustered) {
|
if (clustered) {
|
||||||
opts.redisOptions = {}
|
opts.redisOptions = {}
|
||||||
opts.redisOptions.tls = {}
|
opts.redisOptions.tls = {}
|
||||||
opts.redisOptions.password = REDIS_PASSWORD
|
opts.redisOptions.password = password
|
||||||
opts.slotsRefreshTimeout = SLOT_REFRESH_MS
|
opts.slotsRefreshTimeout = SLOT_REFRESH_MS
|
||||||
opts.dnsLookup = (address, callback) => callback(null, address)
|
opts.dnsLookup = (address, callback) => callback(null, address)
|
||||||
} else {
|
} else {
|
||||||
opts.host = host
|
opts.host = host
|
||||||
opts.port = port
|
opts.port = port
|
||||||
opts.password = REDIS_PASSWORD
|
opts.password = password
|
||||||
}
|
}
|
||||||
return { opts, host, port, redisProtocolUrl }
|
return { opts, host, port, redisProtocolUrl }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
exports.lookupApiKey = async () => {}
|
|
@ -0,0 +1,33 @@
|
||||||
|
const crypto = require("crypto")
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
|
const ALGO = "aes-256-ctr"
|
||||||
|
const SECRET = env.JWT_SECRET
|
||||||
|
const SEPARATOR = "-"
|
||||||
|
const ITERATIONS = 10000
|
||||||
|
const RANDOM_BYTES = 16
|
||||||
|
const STRETCH_LENGTH = 32
|
||||||
|
|
||||||
|
function stretchString(string, salt) {
|
||||||
|
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.encrypt = input => {
|
||||||
|
const salt = crypto.randomBytes(RANDOM_BYTES)
|
||||||
|
const stretched = stretchString(SECRET, salt)
|
||||||
|
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
|
||||||
|
const base = cipher.update(input)
|
||||||
|
const final = cipher.final()
|
||||||
|
const encrypted = Buffer.concat([base, final]).toString("hex")
|
||||||
|
return `${salt.toString("hex")}${SEPARATOR}${encrypted}`
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.decrypt = input => {
|
||||||
|
const [salt, encrypted] = input.split(SEPARATOR)
|
||||||
|
const saltBuffer = Buffer.from(salt, "hex")
|
||||||
|
const stretched = stretchString(SECRET, saltBuffer)
|
||||||
|
const decipher = crypto.createDecipheriv(ALGO, stretched, saltBuffer)
|
||||||
|
const base = decipher.update(Buffer.from(encrypted, "hex"))
|
||||||
|
const final = decipher.final()
|
||||||
|
return Buffer.concat([base, final]).toString()
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ const PermissionLevels = {
|
||||||
|
|
||||||
// these are the global types, that govern the underlying default behaviour
|
// these are the global types, that govern the underlying default behaviour
|
||||||
const PermissionTypes = {
|
const PermissionTypes = {
|
||||||
|
APP: "app",
|
||||||
TABLE: "table",
|
TABLE: "table",
|
||||||
USER: "user",
|
USER: "user",
|
||||||
AUTOMATION: "automation",
|
AUTOMATION: "automation",
|
||||||
|
|
|
@ -6,7 +6,7 @@ const {
|
||||||
} = require("./db/utils")
|
} = require("./db/utils")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const { options } = require("./middleware/passport/jwt")
|
const { options } = require("./middleware/passport/jwt")
|
||||||
const { createUserEmailView, createUserBuildersView } = require("./db/views")
|
const { queryGlobalView } = require("./db/views")
|
||||||
const { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants")
|
const { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants")
|
||||||
const {
|
const {
|
||||||
getGlobalDB,
|
getGlobalDB,
|
||||||
|
@ -139,40 +139,16 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
throw "Must supply an email address to view"
|
throw "Must supply an email address to view"
|
||||||
}
|
}
|
||||||
const db = getGlobalDB()
|
|
||||||
|
|
||||||
try {
|
return queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
||||||
let users = (
|
key: email.toLowerCase(),
|
||||||
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
|
include_docs: true,
|
||||||
key: email.toLowerCase(),
|
})
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
).rows
|
|
||||||
users = users.map(user => user.doc)
|
|
||||||
return users.length <= 1 ? users[0] : users
|
|
||||||
} catch (err) {
|
|
||||||
if (err != null && err.name === "not_found") {
|
|
||||||
await createUserEmailView(db)
|
|
||||||
return exports.getGlobalUserByEmail(email)
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getBuildersCount = async () => {
|
exports.getBuildersCount = async () => {
|
||||||
const db = getGlobalDB()
|
const builders = await queryGlobalView(ViewNames.BUILDERS)
|
||||||
try {
|
return builders.total_rows
|
||||||
let users = await db.query(`database/${ViewNames.USER_BY_BUILDERS}`)
|
|
||||||
return users.total_rows
|
|
||||||
} catch (err) {
|
|
||||||
if (err != null && err.name === "not_found") {
|
|
||||||
await createUserBuildersView(db)
|
|
||||||
return exports.getBuildersCount()
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.saveUser = async (
|
exports.saveUser = async (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.78",
|
"version": "1.0.91-alpha.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,6 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
|
"@budibase/string-templates": "^1.0.91-alpha.6",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -57,3 +57,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Toast {
|
||||||
|
pointer-events: all;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/toast/dist/index-vars.css"
|
||||||
|
import Portal from "svelte-portal"
|
||||||
|
import { banner } from "../Stores/banner"
|
||||||
|
import Banner from "./Banner.svelte"
|
||||||
|
import { fly } from "svelte/transition"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Portal target=".banner-container">
|
||||||
|
<div class="banner">
|
||||||
|
{#if $banner.message}
|
||||||
|
<div transition:fly={{ y: -30 }}>
|
||||||
|
<Banner
|
||||||
|
type={$banner.type}
|
||||||
|
extraButtonText={$banner.extraButtonText}
|
||||||
|
extraButtonAction={$banner.extraButtonAction}
|
||||||
|
on:change={$banner.onChange}
|
||||||
|
>
|
||||||
|
{$banner.message}
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.banner {
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -29,6 +29,7 @@
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click|preventDefault
|
on:click|preventDefault
|
||||||
on:mouseover={() => (showTooltip = true)}
|
on:mouseover={() => (showTooltip = true)}
|
||||||
|
on:focus={() => (showTooltip = true)}
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let value
|
export let value
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let spectrumTheme
|
export let spectrumTheme
|
||||||
|
export let alignRight = false
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
|
|
||||||
|
@ -133,6 +134,7 @@
|
||||||
use:clickOutside={() => (open = false)}
|
use:clickOutside={() => (open = false)}
|
||||||
transition:fly={{ y: -20, duration: 200 }}
|
transition:fly={{ y: -20, duration: 200 }}
|
||||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||||
|
class:spectrum-Popover--align-right={alignRight}
|
||||||
>
|
>
|
||||||
{#each categories as category}
|
{#each categories as category}
|
||||||
<div class="category">
|
<div class="category">
|
||||||
|
@ -250,6 +252,9 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
.spectrum-Popover--align-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
.colors {
|
.colors {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { slide } from "svelte/transition"
|
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
import Button from "../Button/Button.svelte"
|
import Button from "../Button/Button.svelte"
|
||||||
import Body from "../Typography/Body.svelte"
|
import Body from "../Typography/Body.svelte"
|
||||||
|
@ -7,7 +6,9 @@
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let fillWidth
|
export let fillWidth
|
||||||
|
|
||||||
let visible = false
|
let visible = false
|
||||||
|
|
||||||
export function show() {
|
export function show() {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
return
|
return
|
||||||
|
@ -21,11 +22,27 @@
|
||||||
}
|
}
|
||||||
visible = false
|
visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const easeInOutQuad = x => {
|
||||||
|
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a custom svelte transition here because the built-in slide
|
||||||
|
// transition has a horrible overshoot
|
||||||
|
const slide = () => {
|
||||||
|
return {
|
||||||
|
duration: 360,
|
||||||
|
css: t => {
|
||||||
|
const translation = 100 - Math.round(easeInOutQuad(t) * 100)
|
||||||
|
return `transform: translateY(${translation}%);`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<Portal>
|
<Portal>
|
||||||
<section class:fillWidth class="drawer" transition:slide>
|
<section class:fillWidth class="drawer" transition:slide|local>
|
||||||
<header>
|
<header>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<Heading size="XS">{title}</Heading>
|
<Heading size="XS">{title}</Heading>
|
||||||
|
@ -79,4 +96,12 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -47,7 +47,9 @@
|
||||||
<use xlink:href="#spectrum-css-icon-Dash100" />
|
<use xlink:href="#spectrum-css-icon-Dash100" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="spectrum-Checkbox-label">{text || ""}</span>
|
{#if text}
|
||||||
|
<span class="spectrum-Checkbox-label">{text}</span>
|
||||||
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -54,34 +54,43 @@
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKey} />
|
<svelte:window on:keydown={handleKey} />
|
||||||
|
|
||||||
<!-- These svelte if statements need to be defined like this. -->
|
{#if inline}
|
||||||
<!-- The modal transitions do not work if nested inside more than one "if" -->
|
{#if visible}
|
||||||
{#if visible && inline}
|
<div use:focusFirstInput class="spectrum-Modal inline is-open">
|
||||||
<div use:focusFirstInput class="spectrum-Modal inline is-open">
|
<slot />
|
||||||
<slot />
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{:else if visible}
|
{:else}
|
||||||
|
<!--
|
||||||
|
We cannot conditionally render the portal as this leads to a missing
|
||||||
|
insertion point when using nested modals. Therefore we just conditionally
|
||||||
|
render the content of the portal.
|
||||||
|
It still breaks the modal animation, but its better than soft bricking the
|
||||||
|
screen.
|
||||||
|
-->
|
||||||
<Portal target=".modal-container">
|
<Portal target=".modal-container">
|
||||||
<div
|
{#if visible}
|
||||||
class="spectrum-Underlay is-open"
|
<div
|
||||||
in:fade={{ duration: 200 }}
|
class="spectrum-Underlay is-open"
|
||||||
out:fade|local={{ duration: 200 }}
|
in:fade={{ duration: 200 }}
|
||||||
on:mousedown|self={cancel}
|
out:fade|local={{ duration: 200 }}
|
||||||
>
|
on:mousedown|self={cancel}
|
||||||
<div class="modal-wrapper" on:mousedown|self={cancel}>
|
>
|
||||||
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
|
<div class="modal-wrapper" on:mousedown|self={cancel}>
|
||||||
<slot name="outside" />
|
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
|
||||||
<div
|
<slot name="outside" />
|
||||||
use:focusFirstInput
|
<div
|
||||||
class="spectrum-Modal is-open"
|
use:focusFirstInput
|
||||||
in:fly={{ y: 30, duration: 200 }}
|
class="spectrum-Modal is-open"
|
||||||
out:fly|local={{ y: 30, duration: 200 }}
|
in:fly={{ y: 30, duration: 200 }}
|
||||||
>
|
out:fly|local={{ y: 30, duration: 200 }}
|
||||||
<slot />
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</Portal>
|
</Portal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -165,4 +165,8 @@
|
||||||
.secondary-action {
|
.secondary-action {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spectrum-Dialog-buttonGroup {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
export function createBannerStore() {
|
||||||
|
const DEFAULT_CONFIG = {}
|
||||||
|
|
||||||
|
const banner = writable(DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
const show = async (
|
||||||
|
// eslint-disable-next-line
|
||||||
|
config = { message, type, extraButtonText, extraButtonAction, onChange }
|
||||||
|
) => {
|
||||||
|
banner.update(store => {
|
||||||
|
return {
|
||||||
|
...store,
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showStatus = async () => {
|
||||||
|
const config = {
|
||||||
|
message: "Some systems are experiencing issues",
|
||||||
|
type: "negative",
|
||||||
|
extraButtonText: "View Status",
|
||||||
|
extraButtonAction: () => window.open("https://status.budibase.com/"),
|
||||||
|
}
|
||||||
|
|
||||||
|
await show(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: banner.subscribe,
|
||||||
|
showStatus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const banner = createBannerStore()
|
|
@ -60,7 +60,7 @@ export const createNotificationStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function id() {
|
function id() {
|
||||||
return "_" + Math.random().toString(36).substr(2, 9)
|
return "_" + Math.random().toString(36).slice(2, 9)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const notifications = createNotificationStore()
|
export const notifications = createNotificationStore()
|
||||||
|
|
|
@ -17,14 +17,16 @@
|
||||||
{#each attachments as attachment}
|
{#each attachments as attachment}
|
||||||
{#if isImage(attachment.extension)}
|
{#if isImage(attachment.extension)}
|
||||||
<Link quiet target="_blank" href={attachment.url}>
|
<Link quiet target="_blank" href={attachment.url}>
|
||||||
<img src={attachment.url} alt={attachment.extension} />
|
<div class="center">
|
||||||
|
<img src={attachment.url} alt={attachment.extension} />
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
{:else}
|
{:else}
|
||||||
<Tooltip text={attachment.name} direction="right">
|
<Tooltip text={attachment.name} direction="right">
|
||||||
<div class="file">
|
<div class="file">
|
||||||
<Link quiet target="_blank" href={attachment.url}
|
<Link quiet target="_blank" href={attachment.url}>
|
||||||
>{attachment.extension}</Link
|
{attachment.extension}
|
||||||
>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -38,12 +40,15 @@
|
||||||
height: 32px;
|
height: 32px;
|
||||||
max-width: 64px;
|
max-width: 64px;
|
||||||
}
|
}
|
||||||
|
.center,
|
||||||
.file {
|
.file {
|
||||||
height: 32px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
}
|
||||||
|
.file {
|
||||||
|
height: 32px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
color: var(--spectrum-global-color-gray-800);
|
color: var(--spectrum-global-color-gray-800);
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
|
|
@ -7,5 +7,9 @@
|
||||||
<style>
|
<style>
|
||||||
.bold {
|
.bold {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: var(--max-cell-width);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import AttachmentRenderer from "./AttachmentRenderer.svelte"
|
import AttachmentRenderer from "./AttachmentRenderer.svelte"
|
||||||
import ArrayRenderer from "./ArrayRenderer.svelte"
|
import ArrayRenderer from "./ArrayRenderer.svelte"
|
||||||
import InternalRenderer from "./InternalRenderer.svelte"
|
import InternalRenderer from "./InternalRenderer.svelte"
|
||||||
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let row
|
export let row
|
||||||
export let schema
|
export let schema
|
||||||
|
@ -28,10 +29,33 @@
|
||||||
$: type = schema?.type ?? "string"
|
$: type = schema?.type ?? "string"
|
||||||
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
|
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
|
||||||
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
|
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
|
||||||
|
$: width = schema?.width || "150px"
|
||||||
|
$: cellValue = getCellValue(value, schema.template)
|
||||||
|
|
||||||
|
const getCellValue = (value, template) => {
|
||||||
|
if (!template) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return processStringSync(template, { value })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if renderer && (customRenderer || (value != null && value !== ""))}
|
{#if renderer && (customRenderer || (cellValue != null && cellValue !== ""))}
|
||||||
<svelte:component this={renderer} {row} {schema} {value} on:clickrelationship>
|
<div style="--max-cell-width: {schema.width ? 'none' : '200px'};">
|
||||||
<slot />
|
<svelte:component
|
||||||
</svelte:component>
|
this={renderer}
|
||||||
|
{row}
|
||||||
|
{schema}
|
||||||
|
value={cellValue}
|
||||||
|
on:clickrelationship
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</svelte:component>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -3,3 +3,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<code>{value}</code>
|
<code>{value}</code>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
code {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: var(--max-cell-width);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
width: 200px;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -43,11 +43,3 @@
|
||||||
<div on:click|stopPropagation={onClick}>
|
<div on:click|stopPropagation={onClick}>
|
||||||
<Icon size="S" name="Copy" />
|
<Icon size="S" name="Copy" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
div {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -8,9 +8,21 @@
|
||||||
export let allowEditRows = false
|
export let allowEditRows = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if allowSelectRows}
|
<div>
|
||||||
<Checkbox value={selected} />
|
{#if allowSelectRows}
|
||||||
{/if}
|
<Checkbox value={selected} />
|
||||||
{#if allowEditRows}
|
{/if}
|
||||||
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton>
|
{#if allowEditRows}
|
||||||
{/if}
|
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
div {
|
div {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
width: 150px;
|
white-space: nowrap;
|
||||||
|
max-width: var(--max-cell-width);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
import "@spectrum-css/table/dist/index-vars.css"
|
import "@spectrum-css/table/dist/index-vars.css"
|
||||||
import CellRenderer from "./CellRenderer.svelte"
|
import CellRenderer from "./CellRenderer.svelte"
|
||||||
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep, deepGet } from "../helpers"
|
||||||
import { deepGet } from "../helpers"
|
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
||||||
|
import Checkbox from "../Form/Checkbox.svelte"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The expected schema is our normal couch schemas for our tables.
|
* The expected schema is our normal couch schemas for our tables.
|
||||||
|
@ -15,6 +16,11 @@
|
||||||
* sortable: Set to false to disable sorting data by a certain column
|
* sortable: Set to false to disable sorting data by a certain column
|
||||||
* editable: Set to false to disable editing a certain column if the
|
* editable: Set to false to disable editing a certain column if the
|
||||||
* allowEditColumns prop is true
|
* allowEditColumns prop is true
|
||||||
|
* width: the width of the column
|
||||||
|
* align: the alignment of the column
|
||||||
|
* template: a HBS or JS binding to use as the value
|
||||||
|
* background: the background color
|
||||||
|
* color: the text color
|
||||||
*/
|
*/
|
||||||
export let data = []
|
export let data = []
|
||||||
export let schema = {}
|
export let schema = {}
|
||||||
|
@ -26,16 +32,16 @@
|
||||||
export let allowEditRows = true
|
export let allowEditRows = true
|
||||||
export let allowEditColumns = true
|
export let allowEditColumns = true
|
||||||
export let selectedRows = []
|
export let selectedRows = []
|
||||||
export let editColumnTitle = "Edit"
|
|
||||||
export let customRenderers = []
|
export let customRenderers = []
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
|
export let autoSortColumns = true
|
||||||
|
export let compact = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
const rowHeight = 55
|
|
||||||
const headerHeight = 36
|
const headerHeight = 36
|
||||||
const rowPreload = 5
|
$: rowHeight = compact ? 46 : 55
|
||||||
|
|
||||||
// Sorting state
|
// Sorting state
|
||||||
let sortColumn
|
let sortColumn
|
||||||
|
@ -44,33 +50,39 @@
|
||||||
// Table state
|
// Table state
|
||||||
let height = 0
|
let height = 0
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let checkboxStatus = false
|
||||||
|
|
||||||
$: schema = fixSchema(schema)
|
$: schema = fixSchema(schema)
|
||||||
$: if (!loading) loaded = true
|
$: if (!loading) loaded = true
|
||||||
$: rows = data ?? []
|
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
||||||
$: visibleRowCount = getVisibleRowCount(loaded, height, rows.length, rowCount)
|
$: rows = fields?.length ? data || [] : []
|
||||||
$: contentStyle = getContentStyle(visibleRowCount, rowCount)
|
$: totalRowCount = rows?.length || 0
|
||||||
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
$: visibleRowCount = getVisibleRowCount(
|
||||||
$: fields = getFields(schema, showAutoColumns)
|
loaded,
|
||||||
$: showEditColumn = allowEditRows || allowSelectRows
|
height,
|
||||||
|
rows.length,
|
||||||
// Scrolling state
|
rowCount,
|
||||||
let timeout
|
rowHeight
|
||||||
let nextScrollTop = 0
|
|
||||||
let scrollTop = 0
|
|
||||||
$: firstVisibleRow = calculateFirstVisibleRow(scrollTop)
|
|
||||||
$: lastVisibleRow = calculateLastVisibleRow(
|
|
||||||
firstVisibleRow,
|
|
||||||
visibleRowCount,
|
|
||||||
rows.length
|
|
||||||
)
|
)
|
||||||
|
$: heightStyle = getHeightStyle(
|
||||||
|
visibleRowCount,
|
||||||
|
rowCount,
|
||||||
|
totalRowCount,
|
||||||
|
rowHeight
|
||||||
|
)
|
||||||
|
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
||||||
|
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
|
||||||
|
$: showEditColumn = allowEditRows || allowSelectRows
|
||||||
|
$: cellStyles = computeCellStyles(schema)
|
||||||
|
|
||||||
// Reset state when data changes
|
// Deselect the "select all" checkbox when the user navigates to a new page
|
||||||
$: rows.length, reset()
|
$: {
|
||||||
const reset = () => {
|
let checkRowCount = rows.filter(o1 =>
|
||||||
nextScrollTop = 0
|
selectedRows.some(o2 => o1._id === o2._id)
|
||||||
scrollTop = 0
|
)
|
||||||
clearTimeout(timeout)
|
if (checkRowCount.length === 0) {
|
||||||
timeout = null
|
checkboxStatus = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fixSchema = schema => {
|
const fixSchema = schema => {
|
||||||
|
@ -91,7 +103,7 @@
|
||||||
return fixedSchema
|
return fixedSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
const getVisibleRowCount = (loaded, height, allRows, rowCount) => {
|
const getVisibleRowCount = (loaded, height, allRows, rowCount, rowHeight) => {
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return rowCount || 0
|
return rowCount || 0
|
||||||
}
|
}
|
||||||
|
@ -101,11 +113,33 @@
|
||||||
return Math.min(allRows, Math.ceil(height / rowHeight))
|
return Math.min(allRows, Math.ceil(height / rowHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContentStyle = (visibleRows, rowCount) => {
|
const getHeightStyle = (
|
||||||
if (!rowCount || !visibleRows) {
|
visibleRowCount,
|
||||||
|
rowCount,
|
||||||
|
totalRowCount,
|
||||||
|
rowHeight
|
||||||
|
) => {
|
||||||
|
if (!rowCount || !visibleRowCount || totalRowCount <= rowCount) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return `height: ${headerHeight + visibleRows * (rowHeight + 1)}px;`
|
return `height: ${headerHeight + visibleRowCount * rowHeight}px;`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGridStyle = (fields, schema, showEditColumn) => {
|
||||||
|
let style = "grid-template-columns:"
|
||||||
|
if (showEditColumn) {
|
||||||
|
style += " auto"
|
||||||
|
}
|
||||||
|
fields?.forEach(field => {
|
||||||
|
const fieldSchema = schema[field]
|
||||||
|
if (fieldSchema.width) {
|
||||||
|
style += ` ${fieldSchema.width}`
|
||||||
|
} else {
|
||||||
|
style += " minmax(auto, 1fr)"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
style += ";"
|
||||||
|
return style
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortRows = (rows, sortColumn, sortOrder) => {
|
const sortRows = (rows, sortColumn, sortOrder) => {
|
||||||
|
@ -144,14 +178,14 @@
|
||||||
return name || ""
|
return name || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFields = (schema, showAutoColumns) => {
|
const getFields = (schema, showAutoColumns, autoSortColumns) => {
|
||||||
let columns = []
|
let columns = []
|
||||||
let autoColumns = []
|
let autoColumns = []
|
||||||
Object.entries(schema || {}).forEach(([field, fieldSchema]) => {
|
Object.entries(schema || {}).forEach(([field, fieldSchema]) => {
|
||||||
if (!field || !fieldSchema) {
|
if (!field || !fieldSchema) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!fieldSchema?.autocolumn) {
|
if (!autoSortColumns || !fieldSchema?.autocolumn) {
|
||||||
columns.push(fieldSchema)
|
columns.push(fieldSchema)
|
||||||
} else if (showAutoColumns) {
|
} else if (showAutoColumns) {
|
||||||
autoColumns.push(fieldSchema)
|
autoColumns.push(fieldSchema)
|
||||||
|
@ -172,28 +206,6 @@
|
||||||
.map(column => column.name)
|
.map(column => column.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onScroll = event => {
|
|
||||||
nextScrollTop = event.target.scrollTop
|
|
||||||
if (timeout) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
scrollTop = nextScrollTop
|
|
||||||
timeout = null
|
|
||||||
}, 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
const calculateFirstVisibleRow = scrollTop => {
|
|
||||||
return Math.max(Math.floor(scrollTop / (rowHeight + 1)) - rowPreload, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const calculateLastVisibleRow = (firstRow, visibleRowCount, allRowCount) => {
|
|
||||||
if (visibleRowCount === 0) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return Math.min(firstRow + visibleRowCount + 2 * rowPreload, allRowCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
const editColumn = (e, field) => {
|
const editColumn = (e, field) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
dispatch("editcolumn", field)
|
dispatch("editcolumn", field)
|
||||||
|
@ -208,176 +220,270 @@
|
||||||
if (!allowSelectRows) {
|
if (!allowSelectRows) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (selectedRows.includes(row)) {
|
if (selectedRows.some(selectedRow => selectedRow._id === row._id)) {
|
||||||
selectedRows = selectedRows.filter(selectedRow => selectedRow !== row)
|
selectedRows = selectedRows.filter(
|
||||||
|
selectedRow => selectedRow._id !== row._id
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
selectedRows = [...selectedRows, row]
|
selectedRows = [...selectedRows, row]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleSelectAll = e => {
|
||||||
|
const select = !!e.detail
|
||||||
|
if (select) {
|
||||||
|
// Add any rows which are not already in selected rows
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (selectedRows.findIndex(x => x._id === row._id) === -1) {
|
||||||
|
selectedRows.push(row)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Remove any rows from selected rows that are in the current data set
|
||||||
|
selectedRows = selectedRows.filter(el =>
|
||||||
|
rows.every(f => f._id !== el._id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const computeCellStyles = schema => {
|
||||||
|
let styles = {}
|
||||||
|
Object.keys(schema || {}).forEach(field => {
|
||||||
|
styles[field] = ""
|
||||||
|
if (schema[field].color) {
|
||||||
|
styles[field] += `color: ${schema[field].color};`
|
||||||
|
}
|
||||||
|
if (schema[field].background) {
|
||||||
|
styles[field] += `background-color: ${schema[field].background};`
|
||||||
|
}
|
||||||
|
if (schema[field].align === "Center") {
|
||||||
|
styles[field] += "justify-content: center; text-align: center;"
|
||||||
|
}
|
||||||
|
if (schema[field].align === "Right") {
|
||||||
|
styles[field] += "justify-content: flex-end; text-align: right;"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return styles
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper" bind:offsetHeight={height}>
|
<div
|
||||||
|
class="wrapper"
|
||||||
|
class:wrapper--quiet={quiet}
|
||||||
|
class:wrapper--compact={compact}
|
||||||
|
bind:offsetHeight={height}
|
||||||
|
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
||||||
|
>
|
||||||
{#if !loaded}
|
{#if !loaded}
|
||||||
<div class="loading" style={contentStyle} />
|
<div class="loading" style={heightStyle}>
|
||||||
|
<ProgressCircle />
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div class="spectrum-Table" style={`${heightStyle}${gridStyle}`}>
|
||||||
on:scroll={onScroll}
|
{#if fields.length}
|
||||||
class:quiet
|
<div class="spectrum-Table-head">
|
||||||
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
{#if showEditColumn}
|
||||||
class="container"
|
<div
|
||||||
>
|
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
|
||||||
<div style={contentStyle}>
|
>
|
||||||
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}>
|
{#if allowSelectRows}
|
||||||
{#if fields.length}
|
<Checkbox
|
||||||
<thead class="spectrum-Table-head">
|
bind:value={checkboxStatus}
|
||||||
<tr>
|
on:change={toggleSelectAll}
|
||||||
{#if showEditColumn}
|
/>
|
||||||
<th class="spectrum-Table-headCell">
|
{:else}
|
||||||
<div class="spectrum-Table-headCell-content">
|
Edit
|
||||||
{editColumnTitle || ""}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
|
||||||
{/if}
|
|
||||||
{#each fields as field}
|
|
||||||
<th
|
|
||||||
class="spectrum-Table-headCell"
|
|
||||||
class:is-sortable={schema[field].sortable !== false}
|
|
||||||
class:is-sorted-desc={sortColumn === field &&
|
|
||||||
sortOrder === "Descending"}
|
|
||||||
class:is-sorted-asc={sortColumn === field &&
|
|
||||||
sortOrder === "Ascending"}
|
|
||||||
on:click={() => sortBy(schema[field])}
|
|
||||||
>
|
|
||||||
<div class="spectrum-Table-headCell-content">
|
|
||||||
<div class="title">{getDisplayName(schema[field])}</div>
|
|
||||||
{#if schema[field]?.autocolumn}
|
|
||||||
<svg
|
|
||||||
class="spectrum-Icon spectrum-Table-autoIcon"
|
|
||||||
focusable="false"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-MagicWand" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
{#if sortColumn === field}
|
|
||||||
<svg
|
|
||||||
class="spectrum-Icon spectrum-UIIcon-ArrowDown100 spectrum-Table-sortedIcon"
|
|
||||||
focusable="false"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-css-icon-Arrow100" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
{#if allowEditColumns && schema[field]?.editable !== false}
|
|
||||||
<svg
|
|
||||||
class="spectrum-Icon spectrum-Table-editIcon"
|
|
||||||
focusable="false"
|
|
||||||
on:click={e => editColumn(e, field)}
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-Edit" />
|
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{/if}
|
{/if}
|
||||||
<tbody class="spectrum-Table-body">
|
{#each fields as field}
|
||||||
{#if sortedRows?.length && fields.length}
|
<div
|
||||||
{#each sortedRows as row, idx}
|
class="spectrum-Table-headCell"
|
||||||
<tr
|
class:spectrum-Table-headCell--alignCenter={schema[field]
|
||||||
on:click={() => dispatch("click", row)}
|
.align === "Center"}
|
||||||
on:click={() => toggleSelectRow(row)}
|
class:spectrum-Table-headCell--alignRight={schema[field].align ===
|
||||||
class="spectrum-Table-row"
|
"Right"}
|
||||||
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow}
|
class:is-sortable={schema[field].sortable !== false}
|
||||||
|
class:is-sorted-desc={sortColumn === field &&
|
||||||
|
sortOrder === "Descending"}
|
||||||
|
class:is-sorted-asc={sortColumn === field &&
|
||||||
|
sortOrder === "Ascending"}
|
||||||
|
on:click={() => sortBy(schema[field])}
|
||||||
|
>
|
||||||
|
<div class="title">{getDisplayName(schema[field])}</div>
|
||||||
|
{#if schema[field]?.autocolumn}
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Table-autoIcon"
|
||||||
|
focusable="false"
|
||||||
>
|
>
|
||||||
{#if idx >= firstVisibleRow && idx <= lastVisibleRow}
|
<use xlink:href="#spectrum-icon-18-MagicWand" />
|
||||||
{#if showEditColumn}
|
</svg>
|
||||||
<td
|
{/if}
|
||||||
class="spectrum-Table-cell spectrum-Table-cell--divider"
|
{#if sortColumn === field}
|
||||||
>
|
<svg
|
||||||
<div class="spectrum-Table-cell-content">
|
class="spectrum-Icon spectrum-UIIcon-ArrowDown100 spectrum-Table-sortedIcon"
|
||||||
<SelectEditRenderer
|
focusable="false"
|
||||||
data={row}
|
aria-hidden="true"
|
||||||
selected={selectedRows.includes(row)}
|
>
|
||||||
onToggleSelection={() => toggleSelectRow(row)}
|
<use xlink:href="#spectrum-css-icon-Arrow100" />
|
||||||
onEdit={e => editRow(e, row)}
|
</svg>
|
||||||
{allowSelectRows}
|
{/if}
|
||||||
{allowEditRows}
|
{#if allowEditColumns && schema[field]?.editable !== false}
|
||||||
/>
|
<svg
|
||||||
</div>
|
class="spectrum-Icon spectrum-Table-editIcon"
|
||||||
</td>
|
focusable="false"
|
||||||
{/if}
|
on:click={e => editColumn(e, field)}
|
||||||
{#each fields as field}
|
>
|
||||||
<td
|
<use xlink:href="#spectrum-icon-18-Edit" />
|
||||||
class="spectrum-Table-cell"
|
</svg>
|
||||||
class:spectrum-Table-cell--divider={!!schema[field]
|
{/if}
|
||||||
.divider}
|
</div>
|
||||||
>
|
{/each}
|
||||||
<div class="spectrum-Table-cell-content">
|
</div>
|
||||||
<CellRenderer
|
{/if}
|
||||||
{customRenderers}
|
{#if sortedRows?.length}
|
||||||
{row}
|
{#each sortedRows as row, idx}
|
||||||
schema={schema[field]}
|
<div
|
||||||
value={deepGet(row, field)}
|
class="spectrum-Table-row"
|
||||||
on:clickrelationship
|
on:click={() => dispatch("click", row)}
|
||||||
>
|
on:click={() => toggleSelectRow(row)}
|
||||||
<slot />
|
>
|
||||||
</CellRenderer>
|
{#if showEditColumn}
|
||||||
</div>
|
<div
|
||||||
</td>
|
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
||||||
{/each}
|
on:click={e => {
|
||||||
{/if}
|
toggleSelectRow(row)
|
||||||
</tr>
|
e.stopPropagation()
|
||||||
{/each}
|
}}
|
||||||
{:else}
|
>
|
||||||
<tr class="placeholder-row">
|
<SelectEditRenderer
|
||||||
{#if showEditColumn}
|
data={row}
|
||||||
<td class="placeholder-offset" />
|
selected={selectedRows.findIndex(
|
||||||
{/if}
|
selectedRow => selectedRow._id === row._id
|
||||||
{#each fields as field}
|
) !== -1}
|
||||||
<td />
|
onEdit={e => editRow(e, row)}
|
||||||
{/each}
|
{allowSelectRows}
|
||||||
<div class="placeholder" class:has-fields={fields.length > 0}>
|
{allowEditRows}
|
||||||
<div class="placeholder-content">
|
/>
|
||||||
<svg
|
</div>
|
||||||
class="spectrum-Icon spectrum-Icon--sizeXXL"
|
|
||||||
focusable="false"
|
|
||||||
>
|
|
||||||
<use xlink:href="#spectrum-icon-18-Table" />
|
|
||||||
</svg>
|
|
||||||
<div>No rows found</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</tr>
|
|
||||||
{/if}
|
{/if}
|
||||||
</tbody>
|
{#each fields as field}
|
||||||
</table>
|
<div
|
||||||
</div>
|
class="spectrum-Table-cell"
|
||||||
|
class:spectrum-Table-cell--divider={!!schema[field].divider}
|
||||||
|
style={cellStyles[field]}
|
||||||
|
>
|
||||||
|
<CellRenderer
|
||||||
|
{customRenderers}
|
||||||
|
{row}
|
||||||
|
schema={schema[field]}
|
||||||
|
value={deepGet(row, field)}
|
||||||
|
on:clickrelationship
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CellRenderer>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div class="placeholder" class:placeholder--no-fields={!fields?.length}>
|
||||||
|
<div class="placeholder-content">
|
||||||
|
<svg class="spectrum-Icon spectrum-Icon--sizeXXL" focusable="false">
|
||||||
|
<use xlink:href="#spectrum-icon-18-Table" />
|
||||||
|
</svg>
|
||||||
|
<div>No rows found</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* Wrapper */
|
||||||
.wrapper {
|
.wrapper {
|
||||||
background-color: var(--spectrum-alias-background-color-secondary);
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
--table-bg: var(--spectrum-global-color-gray-50);
|
||||||
|
--table-border: 1px solid var(--spectrum-alias-border-color-mid);
|
||||||
|
--cell-padding: var(--spectrum-global-dimension-size-250);
|
||||||
|
}
|
||||||
|
.wrapper--quiet {
|
||||||
|
--table-bg: var(--spectrum-alias-background-color-transparent);
|
||||||
|
}
|
||||||
|
.wrapper--compact {
|
||||||
|
--cell-padding: var(--spectrum-global-dimension-size-150);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
/* Loading */
|
||||||
height: 100%;
|
.loading {
|
||||||
position: relative;
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
.spectrum-Table {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
display: grid;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.container.quiet {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.spectrum-Table-head {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.spectrum-Table-head > :first-child {
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
padding-left: var(--cell-padding);
|
||||||
|
}
|
||||||
|
.spectrum-Table-head > :last-child {
|
||||||
|
border-right: 1px solid transparent;
|
||||||
|
padding-right: var(--cell-padding);
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell {
|
||||||
|
height: var(--header-height);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: var(--spectrum-alias-background-color-secondary);
|
||||||
|
z-index: 2;
|
||||||
|
border-bottom: var(--table-border);
|
||||||
|
padding: 0 calc(var(--cell-padding) / 1.33);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell--alignCenter {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell--alignRight {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell--divider {
|
||||||
|
padding-right: var(--cell-padding);
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell--divider + .spectrum-Table-headCell {
|
||||||
|
padding-left: var(--cell-padding);
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell--edit {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell .title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
.spectrum-Table-headCell .spectrum-Icon {
|
.spectrum-Table-headCell .spectrum-Icon {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
margin-left: var(
|
margin-left: var(
|
||||||
|
@ -393,63 +499,93 @@
|
||||||
.spectrum-Table-editIcon {
|
.spectrum-Table-editIcon {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
|
|
||||||
opacity: 1;
|
/* Table rows */
|
||||||
transition: opacity 0.2s ease;
|
.spectrum-Table-row {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.spectrum-Table-row:hover .spectrum-Table-cell {
|
||||||
|
/*background-color: var(--hover-bg) !important;*/
|
||||||
|
}
|
||||||
|
.spectrum-Table-row:hover .spectrum-Table-cell:after {
|
||||||
|
background-color: var(--spectrum-alias-highlight-hover);
|
||||||
|
}
|
||||||
|
.wrapper--quiet .spectrum-Table-row {
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.spectrum-Table-row > :first-child {
|
||||||
|
border-left: var(--table-border);
|
||||||
|
padding-left: var(--cell-padding);
|
||||||
|
}
|
||||||
|
.spectrum-Table-row > :last-child {
|
||||||
|
border-right: var(--table-border);
|
||||||
|
padding-right: var(--cell-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
/* Table cells */
|
||||||
vertical-align: middle;
|
.spectrum-Table-cell {
|
||||||
height: var(--header-height);
|
flex: 1 1 auto;
|
||||||
position: sticky;
|
padding: 0 calc(var(--cell-padding) / 1.33);
|
||||||
top: 0;
|
border-top: none;
|
||||||
z-index: 2;
|
border-bottom: none;
|
||||||
background-color: var(--spectrum-alias-background-color-secondary);
|
border-radius: 0;
|
||||||
border-bottom: 1px solid
|
text-overflow: ellipsis;
|
||||||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
|
||||||
}
|
|
||||||
.spectrum-Table-headCell-content {
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
height: var(--row-height);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
user-select: none;
|
gap: 4px;
|
||||||
|
border-bottom: 1px solid var(--spectrum-alias-border-color-mid);
|
||||||
|
background-color: var(--table-bg);
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell-content .title {
|
.spectrum-Table-cell--divider {
|
||||||
overflow: hidden;
|
padding-right: var(--cell-padding);
|
||||||
text-overflow: ellipsis;
|
}
|
||||||
|
.spectrum-Table-cell--divider + .spectrum-Table-cell {
|
||||||
|
padding-left: var(--cell-padding);
|
||||||
|
}
|
||||||
|
.spectrum-Table-cell--edit {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.spectrum-Table-cell:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: background-color
|
||||||
|
var(--spectrum-global-animation-duration-100, 0.13s) ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-row {
|
/* Placeholder */
|
||||||
position: relative;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
.placeholder-row td {
|
|
||||||
border-top: none !important;
|
|
||||||
border-bottom: none !important;
|
|
||||||
}
|
|
||||||
.placeholder-offset {
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border: var(--table-border);
|
||||||
|
border-top: none;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
background-color: var(--table-bg);
|
||||||
}
|
}
|
||||||
.placeholder.has-fields {
|
.placeholder--no-fields {
|
||||||
top: var(--header-height);
|
border-top: var(--table-border);
|
||||||
height: calc(100% - var(--header-height));
|
}
|
||||||
|
.wrapper--quiet .placeholder {
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-content {
|
.placeholder-content {
|
||||||
padding: 20px;
|
padding: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -467,41 +603,4 @@
|
||||||
);
|
);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
tbody tr {
|
|
||||||
height: var(--row-height);
|
|
||||||
}
|
|
||||||
tbody tr.hidden {
|
|
||||||
height: calc(var(--row-height) + 1px);
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
border-bottom: none;
|
|
||||||
border-top: 1px solid
|
|
||||||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
tr:first-child td {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
tr:last-child td {
|
|
||||||
border-bottom: 1px solid
|
|
||||||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
|
||||||
}
|
|
||||||
td.spectrum-Table-cell--divider {
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
.spectrum-Table-cell-content {
|
|
||||||
height: var(--row-height);
|
|
||||||
white-space: nowrap;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
function id() {
|
function id() {
|
||||||
return "_" + Math.random().toString(36).substr(2, 9)
|
return "_" + Math.random().toString(36).slice(2, 9)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -98,3 +98,11 @@ export const deepSet = (obj, key, value) => {
|
||||||
}
|
}
|
||||||
obj[split[split.length - 1]] = value
|
obj[split[split.length - 1]] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deeply clones an object. Functions are not supported.
|
||||||
|
* @param obj the object to clone
|
||||||
|
*/
|
||||||
|
export const cloneDeep = obj => {
|
||||||
|
return JSON.parse(JSON.stringify(obj))
|
||||||
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
||||||
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||||
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
||||||
export { default as Banner } from "./Banner/Banner.svelte"
|
export { default as Banner } from "./Banner/Banner.svelte"
|
||||||
|
export { default as BannerDisplay } from "./Banner/BannerDisplay.svelte"
|
||||||
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
||||||
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
||||||
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
||||||
|
@ -84,6 +85,7 @@ export { default as clickOutside } from "./Actions/click_outside"
|
||||||
|
|
||||||
// Stores
|
// Stores
|
||||||
export { notifications, createNotificationStore } from "./Stores/notifications"
|
export { notifications, createNotificationStore } from "./Stores/notifications"
|
||||||
|
export { banner } from "./Stores/banner"
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
export * as Helpers from "./helpers"
|
export * as Helpers from "./helpers"
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,11 @@
|
||||||
{
|
{
|
||||||
"baseUrl": "http://localhost:10001/builder/",
|
"baseUrl": "http://localhost:4100",
|
||||||
"video": true,
|
"video": false,
|
||||||
"projectId": "bmbemn",
|
"projectId": "bmbemn",
|
||||||
"env": {
|
"env": {
|
||||||
"PORT": "10001",
|
"PORT": "4100",
|
||||||
"JWT_SECRET": "test"
|
"WORKER_PORT": "4200",
|
||||||
|
"JWT_SECRET": "test",
|
||||||
|
"HOST_IP": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
context("Add Multi-Option Datatype", () => {
|
import filterTests from "../support/filterTests"
|
||||||
before(() => {
|
|
||||||
cy.login()
|
|
||||||
cy.createTestApp()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should create a new table, with data", () => {
|
filterTests(['all'], () => {
|
||||||
cy.createTable("Multi Data")
|
context("Add Multi-Option Datatype", () => {
|
||||||
cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
|
before(() => {
|
||||||
cy.addRowMultiValue(["1", "2", "3", "4", "5"])
|
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", () => {
|
it("should add form with multi select picker, containing 5 options", () => {
|
||||||
cy.navigateToFrontend()
|
cy.navigateToFrontend()
|
||||||
|
@ -39,6 +42,7 @@ context("Add Multi-Option Datatype", () => {
|
||||||
cy.getComponent(componentId)
|
cy.getComponent(componentId)
|
||||||
.find(".spectrum-Picker-label")
|
.find(".spectrum-Picker-label")
|
||||||
.contains("(5)")
|
.contains("(5)")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,35 +1,39 @@
|
||||||
context("Add Radio Buttons", () => {
|
import filterTests from "../support/filterTests"
|
||||||
before(() => {
|
|
||||||
cy.login()
|
|
||||||
cy.createTestApp()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should add Radio Buttons options picker on form, add data, and confirm", () => {
|
filterTests(['all'], () => {
|
||||||
cy.navigateToFrontend()
|
context("Add Radio Buttons", () => {
|
||||||
cy.addComponent("Form", "Form")
|
before(() => {
|
||||||
cy.addComponent("Form", "Options Picker").then((componentId) => {
|
cy.login()
|
||||||
// Provide field setting
|
cy.createTestApp()
|
||||||
cy.get(`[data-cy="field-prop-control"]`).type("1")
|
|
||||||
// Open dropdown and select Radio buttons
|
|
||||||
cy.get(`[data-cy="optionsType-prop-control"]`).click().then(() => {
|
|
||||||
cy.get('.spectrum-Popover').contains('Radio buttons')
|
|
||||||
.wait(500)
|
|
||||||
.click()
|
|
||||||
})
|
})
|
||||||
const radioButtonsTotal = 3
|
|
||||||
// Add values and confirm total
|
it("should add Radio Buttons options picker on form, add data, and confirm", () => {
|
||||||
addRadioButtonData(radioButtonsTotal)
|
cy.navigateToFrontend()
|
||||||
cy.getComponent(componentId).find('[type="radio"]')
|
cy.addComponent("Form", "Form")
|
||||||
.should('have.length', radioButtonsTotal)
|
cy.addComponent("Form", "Options Picker").then((componentId) => {
|
||||||
})
|
// Provide field setting
|
||||||
})
|
cy.get(`[data-cy="field-prop-control"]`).type("1")
|
||||||
|
// Open dropdown and select Radio buttons
|
||||||
const addRadioButtonData = (totalRadioButtons) => {
|
cy.get(`[data-cy="optionsType-prop-control"]`).click().then(() => {
|
||||||
cy.get(`[data-cy="optionsSource-prop-control"]`).click().then(() => {
|
cy.get('.spectrum-Popover').contains('Radio buttons')
|
||||||
cy.get('.spectrum-Popover').contains('Custom')
|
|
||||||
.wait(500)
|
.wait(500)
|
||||||
.click()
|
.click()
|
||||||
|
})
|
||||||
|
const radioButtonsTotal = 3
|
||||||
|
// Add values and confirm total
|
||||||
|
addRadioButtonData(radioButtonsTotal)
|
||||||
|
cy.getComponent(componentId).find('[type="radio"]')
|
||||||
|
.should('have.length', radioButtonsTotal)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
cy.addCustomSourceOptions(totalRadioButtons)
|
|
||||||
}
|
const addRadioButtonData = (totalRadioButtons) => {
|
||||||
|
cy.get(`[data-cy="optionsSource-prop-control"]`).click().then(() => {
|
||||||
|
cy.get('.spectrum-Popover').contains('Custom')
|
||||||
|
.wait(500)
|
||||||
|
.click()
|
||||||
|
})
|
||||||
|
cy.addCustomSourceOptions(totalRadioButtons)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import filterTests from "../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(['smoke', 'all'], () => {
|
||||||
|
context("Auto Screens UI", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should generate internal table screens", () => {
|
||||||
|
// Create autogenerated screens from the internal table
|
||||||
|
cy.createAutogeneratedScreens(["Cypress Tests"])
|
||||||
|
// Confirm screens have been auto generated
|
||||||
|
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
||||||
|
cy.get(".nav-items-container").should('contain', 'cypress-tests/:id')
|
||||||
|
.and('contain', 'cypress-tests/new/row')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should generate multiple internal table screens at once", () => {
|
||||||
|
// Create a second internal table
|
||||||
|
const initialTable = "Cypress Tests"
|
||||||
|
const secondTable = "Table Two"
|
||||||
|
cy.createTable(secondTable)
|
||||||
|
// Create autogenerated screens from the internal tables
|
||||||
|
cy.createAutogeneratedScreens([initialTable, secondTable])
|
||||||
|
// Confirm screens have been auto generated
|
||||||
|
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
||||||
|
// Previously generated tables are suffixed with numbers - as expected
|
||||||
|
cy.get(".nav-items-container").should('contain', 'cypress-tests-2/:id')
|
||||||
|
.and('contain', 'cypress-tests-2/new/row')
|
||||||
|
cy.get(".nav-items-container").contains("table-two").click()
|
||||||
|
cy.get(".nav-items-container").should('contain', 'table-two/:id')
|
||||||
|
.and('contain', 'table-two/new/row')
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Cypress.env("TEST_ENV")) {
|
||||||
|
it("should generate data source screens", () => {
|
||||||
|
// Using MySQL data source for testing this
|
||||||
|
const datasource = "MySQL"
|
||||||
|
// Select & configure MySQL data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.addDatasourceConfig(datasource)
|
||||||
|
// Create autogenerated screens from a MySQL table - MySQL contains books table
|
||||||
|
cy.createAutogeneratedScreens(["books"])
|
||||||
|
cy.get(".nav-items-container").contains("books").click()
|
||||||
|
cy.get(".nav-items-container").should('contain', 'books/:id')
|
||||||
|
.and('contain', 'books/new/row')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,43 @@
|
||||||
|
import filterTests from "../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(['all'], () => {
|
||||||
|
context("Change Application Icon and Colour", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should change the icon and colour for an application", () => {
|
||||||
|
// Search for test application
|
||||||
|
cy.searchForApplication("Cypress Tests")
|
||||||
|
cy.get(".appTable")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(1).click()
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Menu").contains("Edit icon").click()
|
||||||
|
// Select random icon
|
||||||
|
cy.get(".grid").within(() => {
|
||||||
|
cy.get(".icon-item").eq(Math.floor(Math.random() * 23) + 1).click()
|
||||||
|
})
|
||||||
|
// Select random colour
|
||||||
|
cy.get(".fill").click()
|
||||||
|
cy.get(".colors").within(() => {
|
||||||
|
cy.get(".color").eq(Math.floor(Math.random() * 33) + 1).click()
|
||||||
|
})
|
||||||
|
cy.intercept('**/applications/**').as('iconChange')
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.wait("@iconChange")
|
||||||
|
cy.get("@iconChange").its('response.statusCode')
|
||||||
|
.should('eq', 200)
|
||||||
|
cy.wait(1000)
|
||||||
|
// Confirm icon has changed from default
|
||||||
|
// Confirm colour has been applied - There is no default colour
|
||||||
|
cy.get(".appTable")
|
||||||
|
.within(() => {
|
||||||
|
cy.get('[aria-label]').eq(0).children()
|
||||||
|
.should('have.attr', 'xlink:href').and('not.contain', '#spectrum-icon-18-Apps')
|
||||||
|
cy.get(".title").children().children()
|
||||||
|
.should('have.attr', 'style').and('contains', 'color')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,8 +1,12 @@
|
||||||
context("Create an Application", () => {
|
import filterTests from '../support/filterTests'
|
||||||
it("should create a new application", () => {
|
|
||||||
cy.login()
|
filterTests(['smoke', 'all'], () => {
|
||||||
cy.createTestApp()
|
context("Create an Application", () => {
|
||||||
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
|
it("should create a new application", () => {
|
||||||
cy.contains("Cypress Tests").should("exist")
|
cy.login()
|
||||||
})
|
cy.createTestApp()
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.contains("Cypress Tests").should("exist")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,66 +1,69 @@
|
||||||
context("Create a automation", () => {
|
import filterTests from "../support/filterTests"
|
||||||
before(() => {
|
|
||||||
cy.login()
|
|
||||||
cy.createTestApp()
|
|
||||||
})
|
|
||||||
|
|
||||||
// https://on.cypress.io/interacting-with-elements
|
filterTests(['smoke', 'all'], () => {
|
||||||
it("should create a automation", () => {
|
context("Create a automation", () => {
|
||||||
cy.createTestTableWithData()
|
before(() => {
|
||||||
cy.wait(2000)
|
cy.login()
|
||||||
cy.contains("Automate").click()
|
cy.createTestApp()
|
||||||
cy.get("[data-cy='new-screen'] > .spectrum-Icon").click()
|
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
|
||||||
cy.get("input").type("Add Row")
|
|
||||||
cy.contains("Row Created").click({ force: true })
|
|
||||||
cy.wait(500)
|
|
||||||
cy.get(".spectrum-Button--cta").click()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Setup trigger
|
it("should create a automation", () => {
|
||||||
cy.contains("Setup").click()
|
cy.createTestTableWithData()
|
||||||
cy.get(".spectrum-Picker-label").click()
|
cy.wait(2000)
|
||||||
cy.wait(500)
|
cy.contains("Automate").click()
|
||||||
cy.contains("dog").click()
|
cy.get("[data-cy='new-screen'] > .spectrum-Icon").click()
|
||||||
cy.wait(2000)
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
// Create action
|
cy.get("input").type("Add Row")
|
||||||
cy.get(".block > .spectrum-Icon").click()
|
cy.contains("Row Created").click({ force: true })
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
cy.wait(500)
|
||||||
cy.wait(1000)
|
cy.get(".spectrum-Button--cta").click()
|
||||||
cy.contains("Create Row").trigger('mouseover').click().click()
|
})
|
||||||
cy.get(".spectrum-Button--cta").click()
|
|
||||||
})
|
|
||||||
cy.contains("Setup").click()
|
|
||||||
cy.get(".spectrum-Picker-label").click()
|
|
||||||
cy.contains("dog").click()
|
|
||||||
cy.get(".spectrum-Textfield-input")
|
|
||||||
.first()
|
|
||||||
.type("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
|
|
||||||
cy.get(".spectrum-Textfield-input")
|
|
||||||
.eq(1)
|
|
||||||
.type("11")
|
|
||||||
cy.contains("Finish and test automation").click()
|
|
||||||
|
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
// Setup trigger
|
||||||
cy.wait(1000)
|
cy.contains("Setup").click()
|
||||||
cy.get(".spectrum-Picker-label").click()
|
cy.get(".spectrum-Picker-label").click()
|
||||||
|
cy.wait(500)
|
||||||
|
cy.contains("dog").click()
|
||||||
|
cy.wait(2000)
|
||||||
|
// Create action
|
||||||
|
cy.get('[aria-label="AddCircle"]').eq(1).click()
|
||||||
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.contains("Create Row").trigger('mouseover').click().click()
|
||||||
|
cy.get(".spectrum-Button--cta").click()
|
||||||
|
})
|
||||||
|
cy.contains("Setup").click()
|
||||||
|
cy.get(".spectrum-Picker-label").eq(1).click()
|
||||||
cy.contains("dog").click()
|
cy.contains("dog").click()
|
||||||
cy.wait(1000)
|
|
||||||
cy.get(".spectrum-Textfield-input")
|
cy.get(".spectrum-Textfield-input")
|
||||||
.first()
|
.first()
|
||||||
.type("automationGoodboy")
|
.type("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
|
||||||
cy.get(".spectrum-Textfield-input")
|
cy.get(".spectrum-Textfield-input")
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.type("11")
|
.type("11")
|
||||||
cy.get(".spectrum-Textfield-input")
|
cy.contains("Finish and test automation").click()
|
||||||
.eq(2)
|
|
||||||
.type("123456")
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
cy.get(".spectrum-Textfield-input")
|
cy.wait(1000)
|
||||||
.eq(3)
|
cy.get(".spectrum-Picker-label").click()
|
||||||
.type("123456")
|
cy.contains("dog").click()
|
||||||
cy.contains("Test").click()
|
cy.wait(1000)
|
||||||
|
cy.get(".spectrum-Textfield-input")
|
||||||
|
.first()
|
||||||
|
.type("automationGoodboy")
|
||||||
|
cy.get(".spectrum-Textfield-input")
|
||||||
|
.eq(1)
|
||||||
|
.type("11")
|
||||||
|
cy.get(".spectrum-Textfield-input")
|
||||||
|
.eq(2)
|
||||||
|
.type("123456")
|
||||||
|
cy.get(".spectrum-Textfield-input")
|
||||||
|
.eq(3)
|
||||||
|
.type("123456")
|
||||||
|
cy.contains("Test").click()
|
||||||
|
})
|
||||||
|
cy.contains("Data").click()
|
||||||
|
cy.contains("automationGoodboy")
|
||||||
})
|
})
|
||||||
cy.contains("Data").click()
|
|
||||||
cy.contains("automationGoodboy")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,58 +1,62 @@
|
||||||
context("Create Bindings", () => {
|
import filterTests from "../support/filterTests"
|
||||||
before(() => {
|
|
||||||
cy.login()
|
|
||||||
cy.createTestApp()
|
|
||||||
cy.navigateToFrontend()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should add a current user binding", () => {
|
filterTests(['smoke', 'all'], () => {
|
||||||
cy.addComponent("Elements", "Paragraph").then(() => {
|
context("Create Bindings", () => {
|
||||||
addSettingBinding("text", "Current User._id")
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
cy.navigateToFrontend()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add a current user binding", () => {
|
||||||
|
cy.addComponent("Elements", "Paragraph").then(() => {
|
||||||
|
addSettingBinding("text", "Current User._id")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle an invalid binding", () => {
|
||||||
|
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||||
|
// Cypress needs to escape curly brackets
|
||||||
|
cy.get("[data-cy=setting-text] input")
|
||||||
|
.type("{{}{{}{{} Current User._id {}}{}}")
|
||||||
|
.blur()
|
||||||
|
cy.getComponent(componentId).should("have.text", "{{{ [user].[_id] }}")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add a URL param binding", () => {
|
||||||
|
const paramName = "foo"
|
||||||
|
cy.createScreen("Test Param", `/test/:${paramName}`)
|
||||||
|
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||||
|
addSettingBinding("text", `URL.${paramName}`)
|
||||||
|
// The builder preview pages don't have a real URL, so all we can do
|
||||||
|
// is check that we were able to bind to the property, and that the
|
||||||
|
// component exists on the page
|
||||||
|
cy.getComponent(componentId).should("have.text", "New Paragraph")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add a binding with a handlebars helper", () => {
|
||||||
|
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||||
|
// Cypress needs to escape curly brackets
|
||||||
|
cy.get("[data-cy=setting-text] input")
|
||||||
|
.type("{{}{{} add 1 2 {}}{}}")
|
||||||
|
.blur()
|
||||||
|
cy.getComponent(componentId).should("have.text", "3")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle an invalid binding", () => {
|
const addSettingBinding = (setting, bindingText, clickOption = true) => {
|
||||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
cy.get(`[data-cy="setting-${setting}"] [data-cy=text-binding-button]`).click()
|
||||||
// Cypress needs to escape curly brackets
|
cy.get(".drawer").within(() => {
|
||||||
cy.get("[data-cy=setting-text] input")
|
if (clickOption) {
|
||||||
.type("{{}{{}{{} Current User._id {}}{}}")
|
cy.contains(bindingText).click()
|
||||||
.blur()
|
cy.get("textarea").should("have.value", `{{ ${bindingText} }}`)
|
||||||
cy.getComponent(componentId).should("have.text", "{{{ [user].[_id] }}")
|
} else {
|
||||||
|
cy.get("textarea").type(bindingText)
|
||||||
|
}
|
||||||
|
cy.contains("Save").click()
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
it("should add a URL param binding", () => {
|
|
||||||
const paramName = "foo"
|
|
||||||
cy.createScreen("Test Param", `/test/:${paramName}`)
|
|
||||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
|
||||||
addSettingBinding("text", `URL.${paramName}`)
|
|
||||||
// The builder preview pages don't have a real URL, so all we can do
|
|
||||||
// is check that we were able to bind to the property, and that the
|
|
||||||
// component exists on the page
|
|
||||||
cy.getComponent(componentId).should("have.text", "New Paragraph")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should add a binding with a handlebars helper", () => {
|
|
||||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
|
||||||
// Cypress needs to escape curly brackets
|
|
||||||
cy.get("[data-cy=setting-text] input")
|
|
||||||
.type("{{}{{} add 1 2 {}}{}}")
|
|
||||||
.blur()
|
|
||||||
cy.getComponent(componentId).should("have.text", "3")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const addSettingBinding = (setting, bindingText, clickOption = true) => {
|
|
||||||
cy.get(`[data-cy="setting-${setting}"] [data-cy=text-binding-button]`).click()
|
|
||||||
cy.get(".drawer").within(() => {
|
|
||||||
if (clickOption) {
|
|
||||||
cy.contains(bindingText).click()
|
|
||||||
cy.get("textarea").should("have.value", `{{ ${bindingText} }}`)
|
|
||||||
} else {
|
|
||||||
cy.get("textarea").type(bindingText)
|
|
||||||
}
|
|
||||||
cy.contains("Save").click()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,92 +1,97 @@
|
||||||
// TODO for now components are skipped, might not be good to keep doing this
|
// TODO for now components are skipped, might not be good to keep doing this
|
||||||
xcontext("Create Components", () => {
|
|
||||||
let headlineId
|
|
||||||
|
|
||||||
before(() => {
|
import filterTests from "../support/filterTests"
|
||||||
cy.login()
|
|
||||||
cy.createTestApp()
|
|
||||||
cy.createTable("dog")
|
|
||||||
cy.addColumn("dog", "name", "Text")
|
|
||||||
cy.addColumn("dog", "age", "Number")
|
|
||||||
cy.addColumn("dog", "type", "Options")
|
|
||||||
cy.navigateToFrontend()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should add a container", () => {
|
filterTests(['all'], () => {
|
||||||
cy.addComponent(null, "Container").then(componentId => {
|
xcontext("Create Components", () => {
|
||||||
cy.getComponent(componentId).should("exist")
|
let headlineId
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
cy.createTable("dog")
|
||||||
|
cy.addColumn("dog", "name", "Text")
|
||||||
|
cy.addColumn("dog", "age", "Number")
|
||||||
|
cy.addColumn("dog", "type", "Options")
|
||||||
|
cy.navigateToFrontend()
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
it("should add a headline", () => {
|
it("should add a container", () => {
|
||||||
cy.addComponent("Elements", "Headline").then(componentId => {
|
cy.addComponent(null, "Container").then(componentId => {
|
||||||
headlineId = componentId
|
cy.getComponent(componentId).should("exist")
|
||||||
cy.getComponent(headlineId).should("exist")
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
it("should change the text of the headline", () => {
|
it("should add a headline", () => {
|
||||||
const text = "Lorem ipsum dolor sit amet."
|
cy.addComponent("Elements", "Headline").then(componentId => {
|
||||||
cy.get("[data-cy=Settings]").click()
|
headlineId = componentId
|
||||||
cy.get("[data-cy=setting-text] input")
|
cy.getComponent(headlineId).should("exist")
|
||||||
.type(text)
|
})
|
||||||
.blur()
|
})
|
||||||
cy.getComponent(headlineId).should("have.text", text)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should change the size of the headline", () => {
|
it("should change the text of the headline", () => {
|
||||||
cy.get("[data-cy=Design]").click()
|
const text = "Lorem ipsum dolor sit amet."
|
||||||
cy.contains("Typography").click()
|
|
||||||
cy.get("[data-cy=font-size-prop-control]").click()
|
|
||||||
cy.contains("60px").click()
|
|
||||||
cy.getComponent(headlineId).should("have.css", "font-size", "60px")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should create a form and reset to match schema", () => {
|
|
||||||
cy.addComponent("Form", "Form").then(() => {
|
|
||||||
cy.get("[data-cy=Settings]").click()
|
cy.get("[data-cy=Settings]").click()
|
||||||
cy.get("[data-cy=setting-dataSource]")
|
cy.get("[data-cy=setting-text] input")
|
||||||
.contains("Choose option")
|
.type(text)
|
||||||
.click()
|
.blur()
|
||||||
cy.get(".dropdown")
|
cy.getComponent(headlineId).should("have.text", text)
|
||||||
.contains("dog")
|
})
|
||||||
.click()
|
|
||||||
cy.addComponent("Form", "Field Group").then(fieldGroupId => {
|
it("should change the size of the headline", () => {
|
||||||
|
cy.get("[data-cy=Design]").click()
|
||||||
|
cy.contains("Typography").click()
|
||||||
|
cy.get("[data-cy=font-size-prop-control]").click()
|
||||||
|
cy.contains("60px").click()
|
||||||
|
cy.getComponent(headlineId).should("have.css", "font-size", "60px")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create a form and reset to match schema", () => {
|
||||||
|
cy.addComponent("Form", "Form").then(() => {
|
||||||
cy.get("[data-cy=Settings]").click()
|
cy.get("[data-cy=Settings]").click()
|
||||||
cy.contains("Update Form Fields").click()
|
cy.get("[data-cy=setting-dataSource]")
|
||||||
cy.get(".modal")
|
.contains("Choose option")
|
||||||
.get("button.primary")
|
|
||||||
.click()
|
.click()
|
||||||
cy.getComponent(fieldGroupId).within(() => {
|
cy.get(".dropdown")
|
||||||
cy.contains("name").should("exist")
|
.contains("dog")
|
||||||
cy.contains("age").should("exist")
|
.click()
|
||||||
cy.contains("type").should("exist")
|
cy.addComponent("Form", "Field Group").then(fieldGroupId => {
|
||||||
|
cy.get("[data-cy=Settings]").click()
|
||||||
|
cy.contains("Update Form Fields").click()
|
||||||
|
cy.get(".modal")
|
||||||
|
.get("button.primary")
|
||||||
|
.click()
|
||||||
|
cy.getComponent(fieldGroupId).within(() => {
|
||||||
|
cy.contains("name").should("exist")
|
||||||
|
cy.contains("age").should("exist")
|
||||||
|
cy.contains("type").should("exist")
|
||||||
|
})
|
||||||
|
cy.getComponent(fieldGroupId)
|
||||||
|
.find("input")
|
||||||
|
.should("have.length", 2)
|
||||||
|
cy.getComponent(fieldGroupId)
|
||||||
|
.find(".spectrum-Picker")
|
||||||
|
.should("have.length", 1)
|
||||||
})
|
})
|
||||||
cy.getComponent(fieldGroupId)
|
|
||||||
.find("input")
|
|
||||||
.should("have.length", 2)
|
|
||||||
cy.getComponent(fieldGroupId)
|
|
||||||
.find(".spectrum-Picker")
|
|
||||||
.should("have.length", 1)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
it("deletes a component", () => {
|
it("deletes a component", () => {
|
||||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||||
cy.get("[data-cy=setting-_instanceName] input")
|
cy.get("[data-cy=setting-_instanceName] input")
|
||||||
.type(componentId)
|
.type(componentId)
|
||||||
.blur()
|
.blur()
|
||||||
cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({
|
cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({
|
||||||
force: true,
|
force: true,
|
||||||
|
})
|
||||||
|
cy.get(".dropdown-container")
|
||||||
|
.contains("Delete")
|
||||||
|
.click()
|
||||||
|
cy.get(".modal")
|
||||||
|
.contains("Delete Component")
|
||||||
|
.click()
|
||||||
|
cy.getComponent(componentId).should("not.exist")
|
||||||
})
|
})
|
||||||
cy.get(".dropdown-container")
|
|
||||||
.contains("Delete")
|
|
||||||
.click()
|
|
||||||
cy.get(".modal")
|
|
||||||
.contains("Delete Component")
|
|
||||||
.click()
|
|
||||||
cy.getComponent(componentId).should("not.exist")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
context("Screen Tests", () => {
|
import filterTests from "../support/filterTests"
|
||||||
before(() => {
|
|
||||||
cy.login()
|
|
||||||
cy.createTestApp()
|
|
||||||
cy.navigateToFrontend()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Should successfully create a screen", () => {
|
filterTests(["smoke", "all"], () => {
|
||||||
cy.createScreen("Test Screen", "/test")
|
context("Screen Tests", () => {
|
||||||
cy.get(".nav-items-container").within(() => {
|
before(() => {
|
||||||
cy.contains("/test").should("exist")
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
cy.navigateToFrontend()
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
it("Should update the url", () => {
|
it("Should successfully create a screen", () => {
|
||||||
cy.createScreen("Test Screen", "test with spaces")
|
cy.createScreen("Test Screen", "/test")
|
||||||
cy.get(".nav-items-container").within(() => {
|
cy.get(".nav-items-container").within(() => {
|
||||||
cy.contains("/test-with-spaces").should("exist")
|
cy.contains("/test").should("exist")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should update the url", () => {
|
||||||
|
cy.createScreen("Test Screen", "test with spaces")
|
||||||
|
cy.get(".nav-items-container").within(() => {
|
||||||
|
cy.contains("/test-with-spaces").should("exist")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,74 +1,113 @@
|
||||||
context("Create a Table", () => {
|
import filterTests from "../support/filterTests"
|
||||||
before(() => {
|
|
||||||
cy.login()
|
|
||||||
cy.createTestApp()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should create a new Table", () => {
|
filterTests(["smoke", "all"], () => {
|
||||||
cy.createTable("dog")
|
context("Create a Table", () => {
|
||||||
cy.wait(1000)
|
before(() => {
|
||||||
// Check if Table exists
|
cy.login()
|
||||||
cy.get(".table-title h1").should("have.text", "dog")
|
cy.createTestApp()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("adds a new column to the table", () => {
|
it("should create a new Table", () => {
|
||||||
cy.addColumn("dog", "name", "Text")
|
cy.createTable("dog")
|
||||||
cy.contains("name").should("be.visible")
|
cy.wait(1000)
|
||||||
})
|
// Check if Table exists
|
||||||
|
cy.get(".table-title h1").should("have.text", "dog")
|
||||||
|
})
|
||||||
|
|
||||||
it("creates a row in the table", () => {
|
it("adds a new column to the table", () => {
|
||||||
cy.addRow(["Rover"])
|
cy.addColumn("dog", "name", "Text")
|
||||||
cy.contains("Rover").should("be.visible")
|
cy.contains("name").should("be.visible")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates a column on the table", () => {
|
it("creates a row in the table", () => {
|
||||||
cy.get(".title").click()
|
cy.addRow(["Rover"])
|
||||||
cy.get(".spectrum-Table-editIcon > use").click()
|
cy.contains("Rover").should("be.visible")
|
||||||
cy.get("input").eq(1).type("updated", { force: true })
|
})
|
||||||
// Unset table display column
|
|
||||||
cy.get(".spectrum-Switch-input").eq(1).click()
|
|
||||||
cy.contains("Save Column").click()
|
|
||||||
cy.contains("nameupdated ").should("contain", "nameupdated")
|
|
||||||
})
|
|
||||||
|
|
||||||
|
it("updates a column on the table", () => {
|
||||||
it("edits a row", () => {
|
cy.get(".title").click()
|
||||||
cy.contains("button", "Edit").click({ force: true })
|
cy.get(".spectrum-Table-editIcon > use").click()
|
||||||
cy.wait(1000)
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
cy.get(".spectrum-Modal input").clear()
|
|
||||||
cy.get(".spectrum-Modal input").type("Updated")
|
|
||||||
cy.contains("Save").click()
|
|
||||||
cy.contains("Updated").should("have.text", "Updated")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("deletes a row", () => {
|
|
||||||
cy.get(".spectrum-Checkbox-input").check({ force: true })
|
|
||||||
cy.contains("Delete 1 row(s)").click()
|
|
||||||
cy.get(".spectrum-Modal").contains("Delete").click()
|
|
||||||
cy.contains("RoverUpdated").should("not.exist")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("deletes a column", () => {
|
cy.get("input").eq(0).type("updated", { force: true })
|
||||||
cy.get(".title").click()
|
// Unset table display column
|
||||||
cy.get(".spectrum-Table-editIcon > use").click()
|
cy.get(".spectrum-Switch-input").eq(1).click()
|
||||||
cy.contains("Delete").click()
|
cy.contains("Save Column").click()
|
||||||
cy.wait(50)
|
|
||||||
cy.get(`[data-cy="delete-column-confirm"]`).type("nameupdated")
|
|
||||||
cy.contains("Delete Column").click()
|
|
||||||
cy.contains("nameupdated").should("not.exist")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("deletes a table", () => {
|
|
||||||
cy.get(".nav-item")
|
|
||||||
.contains("dog")
|
|
||||||
.parents(".nav-item")
|
|
||||||
.first()
|
|
||||||
.within(() => {
|
|
||||||
cy.get(".actions .spectrum-Icon").click({ force: true })
|
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Menu > :nth-child(2)").click()
|
cy.contains("nameupdated ").should("contain", "nameupdated")
|
||||||
cy.get(`[data-cy="delete-table-confirm"]`).type("dog")
|
})
|
||||||
cy.contains("Delete Table").click()
|
|
||||||
cy.contains("dog").should("not.exist")
|
it("edits a row", () => {
|
||||||
|
cy.contains("button", "Edit").click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".spectrum-Modal input").clear()
|
||||||
|
cy.get(".spectrum-Modal input").type("Updated")
|
||||||
|
cy.contains("Save").click()
|
||||||
|
cy.contains("Updated").should("have.text", "Updated")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deletes a row", () => {
|
||||||
|
cy.get(".spectrum-Checkbox-input").check({ force: true })
|
||||||
|
cy.contains("Delete 1 row(s)").click()
|
||||||
|
cy.get(".spectrum-Modal").contains("Delete").click()
|
||||||
|
cy.contains("RoverUpdated").should("not.exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Cypress.env("TEST_ENV")) {
|
||||||
|
// No Pagination in CI - Test env only for the next two tests
|
||||||
|
it("Adds 15 rows and checks pagination", () => {
|
||||||
|
// 10 rows per page, 15 rows should create 2 pages within table
|
||||||
|
const totalRows = 16
|
||||||
|
for (let i = 1; i < totalRows; i++) {
|
||||||
|
cy.addRow([i])
|
||||||
|
}
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".spectrum-Pagination").within(() => {
|
||||||
|
cy.get(".spectrum-ActionButton").eq(1).click()
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Pagination").within(() => {
|
||||||
|
cy.get(".spectrum-Body--secondary").contains("Page 2")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Deletes rows and checks pagination", () => {
|
||||||
|
// Delete rows, removing second page of rows from table
|
||||||
|
const deleteRows = 5
|
||||||
|
cy.get(".spectrum-Checkbox-input").check({ force: true })
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
cy.contains("Delete 5 row(s)").click()
|
||||||
|
cy.get(".spectrum-Modal").contains("Delete").click()
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
// Confirm table only has one page
|
||||||
|
cy.get(".spectrum-Pagination").within(() => {
|
||||||
|
cy.get(".spectrum-ActionButton").eq(1).should("not.be.enabled")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it("deletes a column", () => {
|
||||||
|
const columnName = "nameupdated"
|
||||||
|
cy.get(".title").click()
|
||||||
|
cy.get(".spectrum-Table-editIcon > use").click()
|
||||||
|
cy.contains("Delete").click()
|
||||||
|
cy.get('[data-cy="delete-column-confirm"]').type(columnName)
|
||||||
|
cy.contains("Delete Column").click()
|
||||||
|
cy.contains("nameupdated").should("not.exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deletes a table", () => {
|
||||||
|
cy.get(".nav-item")
|
||||||
|
.contains("dog")
|
||||||
|
.parents(".nav-item")
|
||||||
|
.first()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".actions .spectrum-Icon").click({ force: true })
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Menu > :nth-child(2)").click()
|
||||||
|
cy.get('[data-cy="delete-table-confirm"]').type("dog")
|
||||||
|
cy.contains("Delete Table").click()
|
||||||
|
cy.contains("dog").should("not.exist")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
context("Create a User", () => {
|
|
||||||
before(() => {
|
|
||||||
cy.login()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should create a user", () => {
|
|
||||||
cy.createUser("bbuser@test.com")
|
|
||||||
cy.contains("bbuser").should("be.visible")
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
import filterTests from "../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["smoke", "all"], () => {
|
||||||
|
context("Create a User and Assign Roles", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create a user", () => {
|
||||||
|
cy.createUser("bbuser@test.com")
|
||||||
|
cy.get(".spectrum-Table").should("contain", "bbuser")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should confirm there is No Access for a New User", () => {
|
||||||
|
// Click into the user
|
||||||
|
cy.contains("bbuser").click()
|
||||||
|
cy.wait(500)
|
||||||
|
// Get No Access table - Confirm it has apps in it
|
||||||
|
cy.get(".spectrum-Table").eq(1).should("not.contain", "No rows found")
|
||||||
|
// Get Configure Roles table - Confirm it has no apps
|
||||||
|
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should assign role types", () => {
|
||||||
|
// 3 apps minimum required - to assign an app to each role type
|
||||||
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
|
.its("body")
|
||||||
|
.then(val => {
|
||||||
|
if (val.length < 3) {
|
||||||
|
for (let i = 1; i < 3; i++) {
|
||||||
|
const uuid = () => Cypress._.random(0, 1e6)
|
||||||
|
const name = uuid()
|
||||||
|
cy.createApp(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Navigate back to the user
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-SideNav").contains("Users").click()
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Table").contains("bbuser").click()
|
||||||
|
cy.wait(1000)
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-cell")
|
||||||
|
.eq(0)
|
||||||
|
.click()
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Dialog-grid")
|
||||||
|
.contains("Choose an option")
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.wait(1000)
|
||||||
|
if (i == 0) {
|
||||||
|
cy.get(".spectrum-Popover").contains("Admin").click()
|
||||||
|
}
|
||||||
|
if (i == 1) {
|
||||||
|
cy.get(".spectrum-Popover").contains("Power").click()
|
||||||
|
}
|
||||||
|
if (i == 2) {
|
||||||
|
cy.get(".spectrum-Popover").contains("Basic").click()
|
||||||
|
}
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Update role")
|
||||||
|
.click({ force: true })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Confirm roles exist within Configure roles table
|
||||||
|
cy.wait(2000)
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(0)
|
||||||
|
.within(assginedRoles => {
|
||||||
|
expect(assginedRoles).to.contain("Admin")
|
||||||
|
expect(assginedRoles).to.contain("Power")
|
||||||
|
expect(assginedRoles).to.contain("Basic")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should unassign role types", () => {
|
||||||
|
// Set each app within Configure roles table to 'No Access'
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.then(len => {
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-cell")
|
||||||
|
.eq(0)
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.get(".spectrum-Picker").eq(1).click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Popover").contains("No Access").click()
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Update role")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Confirm Configure roles table no longer has any apps in it
|
||||||
|
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should enable Developer access", () => {
|
||||||
|
// Enable Developer access
|
||||||
|
cy.get(".field")
|
||||||
|
.eq(4)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Switch-input").click({ force: true })
|
||||||
|
})
|
||||||
|
// No Access table should now be empty
|
||||||
|
cy.get(".container")
|
||||||
|
.contains("No Access")
|
||||||
|
.parent()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Table").contains("No rows found")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Each app within Configure roles should have Admin access
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.then(len => {
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.eq(i)
|
||||||
|
.contains("Admin")
|
||||||
|
cy.wait(500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should disable Developer access", () => {
|
||||||
|
// Disable Developer access
|
||||||
|
cy.get(".field")
|
||||||
|
.eq(4)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Switch-input").click({ force: true })
|
||||||
|
})
|
||||||
|
// Configure roles table should now be empty
|
||||||
|
cy.get(".container")
|
||||||
|
.contains("Configure roles")
|
||||||
|
.parent()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Table").contains("No rows found")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should delete a user", () => {
|
||||||
|
// Click Delete user button
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Delete user")
|
||||||
|
.click({ force: true })
|
||||||
|
.then(() => {
|
||||||
|
// Confirm deletion within modal
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Delete user")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.wait(4000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Table").should("not.have.text", "bbuser")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,152 +1,156 @@
|
||||||
context("Create a View", () => {
|
import filterTests from "../support/filterTests"
|
||||||
before(() => {
|
|
||||||
cy.login()
|
|
||||||
cy.createTestApp()
|
|
||||||
cy.createTable("data")
|
|
||||||
cy.addColumn("data", "group", "Text")
|
|
||||||
cy.addColumn("data", "age", "Number")
|
|
||||||
cy.addColumn("data", "rating", "Number")
|
|
||||||
|
|
||||||
// 6 Rows
|
filterTests(['smoke', 'all'], () => {
|
||||||
cy.addRow(["Students", 25, 1])
|
context("Create a View", () => {
|
||||||
cy.addRow(["Students", 20, 3])
|
before(() => {
|
||||||
cy.addRow(["Students", 18, 6])
|
cy.login()
|
||||||
cy.addRow(["Students", 25, 2])
|
cy.createTestApp()
|
||||||
cy.addRow(["Teachers", 49, 5])
|
cy.createTable("data")
|
||||||
cy.addRow(["Teachers", 36, 3])
|
cy.addColumn("data", "group", "Text")
|
||||||
})
|
cy.addColumn("data", "age", "Number")
|
||||||
|
cy.addColumn("data", "rating", "Number")
|
||||||
|
|
||||||
it("creates a view", () => {
|
// 6 Rows
|
||||||
cy.contains("Create view").click()
|
cy.addRow(["Students", 25, 1])
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
cy.addRow(["Students", 20, 3])
|
||||||
cy.get("input").type("Test View")
|
cy.addRow(["Students", 18, 6])
|
||||||
cy.get("button").contains("Create View").click({ force: true })
|
cy.addRow(["Students", 25, 2])
|
||||||
|
cy.addRow(["Teachers", 49, 5])
|
||||||
|
cy.addRow(["Teachers", 36, 3])
|
||||||
})
|
})
|
||||||
cy.get(".table-title h1").contains("Test View")
|
|
||||||
cy.get(".title").then($headers => {
|
it("creates a view", () => {
|
||||||
expect($headers).to.have.length(3)
|
cy.contains("Create view").click()
|
||||||
const headers = Array.from($headers).map(header =>
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
header.textContent.trim()
|
cy.get("input").type("Test View")
|
||||||
)
|
cy.get("button").contains("Create View").click({ force: true })
|
||||||
expect(removeSpacing(headers)).to.deep.eq(["group", "age", "rating"])
|
})
|
||||||
|
cy.get(".table-title h1").contains("Test View")
|
||||||
|
cy.get(".title").then($headers => {
|
||||||
|
expect($headers).to.have.length(3)
|
||||||
|
const headers = Array.from($headers).map(header =>
|
||||||
|
header.textContent.trim()
|
||||||
|
)
|
||||||
|
expect(removeSpacing(headers)).to.deep.eq(["group", "age", "rating"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("filters the view by age over 10", () => {
|
||||||
|
cy.contains("Filter").click()
|
||||||
|
cy.contains("Add Filter").click()
|
||||||
|
|
||||||
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
|
cy.get(".spectrum-Picker-label").eq(0).click()
|
||||||
|
cy.contains("age").click({ force: true })
|
||||||
|
|
||||||
|
cy.get(".spectrum-Picker-label").eq(1).click()
|
||||||
|
cy.contains("More Than").click({ force: true })
|
||||||
|
|
||||||
|
cy.get("input").type(18)
|
||||||
|
cy.contains("Save").click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".spectrum-Table-row").get($values => {
|
||||||
|
expect($values).to.have.length(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates a stats calculation view based on age", () => {
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.contains("Calculate").click()
|
||||||
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
|
cy.get(".spectrum-Picker-label").eq(0).click()
|
||||||
|
cy.contains("Statistics").click()
|
||||||
|
|
||||||
|
cy.get(".spectrum-Picker-label").eq(1).click()
|
||||||
|
cy.contains("age").click({ force: true })
|
||||||
|
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
})
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get(".title").then($headers => {
|
||||||
|
expect($headers).to.have.length(7)
|
||||||
|
const headers = Array.from($headers).map(header =>
|
||||||
|
header.textContent.trim()
|
||||||
|
)
|
||||||
|
expect(removeSpacing(headers)).to.deep.eq([
|
||||||
|
"field",
|
||||||
|
"sum",
|
||||||
|
"min",
|
||||||
|
"max",
|
||||||
|
"count",
|
||||||
|
"sumsqr",
|
||||||
|
"avg",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Table-cell").then($values => {
|
||||||
|
let values = Array.from($values).map(header => header.textContent.trim())
|
||||||
|
expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("groups the view by group", () => {
|
||||||
|
cy.contains("Group by").click()
|
||||||
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
|
cy.get(".spectrum-Picker-label").eq(0).click()
|
||||||
|
cy.contains("group").click()
|
||||||
|
cy.contains("Save").click()
|
||||||
|
})
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.contains("Students").should("be.visible")
|
||||||
|
cy.contains("Teachers").should("be.visible")
|
||||||
|
|
||||||
|
cy.get(".spectrum-Table-cell").then($values => {
|
||||||
|
let values = Array.from($values).map(header => header.textContent.trim())
|
||||||
|
expect(values).to.deep.eq([
|
||||||
|
"Students",
|
||||||
|
"70",
|
||||||
|
"20",
|
||||||
|
"25",
|
||||||
|
"3",
|
||||||
|
"1650",
|
||||||
|
"23.333333333333332",
|
||||||
|
"Teachers",
|
||||||
|
"85",
|
||||||
|
"36",
|
||||||
|
"49",
|
||||||
|
"2",
|
||||||
|
"3697",
|
||||||
|
"42.5",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renames a view", () => {
|
||||||
|
cy.contains(".nav-item", "Test View")
|
||||||
|
.find(".actions .icon")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.get(".spectrum-Menu-itemLabel").contains("Edit").click()
|
||||||
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
|
cy.get("input").type(" Updated")
|
||||||
|
cy.contains("Save").click()
|
||||||
|
})
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.contains("Test View Updated").should("be.visible")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("deletes a view", () => {
|
||||||
|
cy.contains(".nav-item", "Test View Updated")
|
||||||
|
.find(".actions .icon")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.contains("Delete").click()
|
||||||
|
cy.contains("Delete View").click()
|
||||||
|
cy.wait(500)
|
||||||
|
cy.contains("TestView Updated").should("not.exist")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("filters the view by age over 10", () => {
|
function removeSpacing(headers) {
|
||||||
cy.contains("Filter").click()
|
let newHeaders = []
|
||||||
cy.contains("Add Filter").click()
|
for (let header of headers) {
|
||||||
|
newHeaders.push(header.replace(/\s\s+/g, " "))
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
}
|
||||||
cy.get(".spectrum-Picker-label").eq(0).click()
|
return newHeaders
|
||||||
cy.contains("age").click({ force: true })
|
|
||||||
|
|
||||||
cy.get(".spectrum-Picker-label").eq(1).click()
|
|
||||||
cy.contains("More Than").click({ force: true })
|
|
||||||
|
|
||||||
cy.get("input").type(18)
|
|
||||||
cy.contains("Save").click()
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.get(".spectrum-Table-row").get($values => {
|
|
||||||
expect($values).to.have.length(5)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("creates a stats calculation view based on age", () => {
|
|
||||||
cy.wait(1000)
|
|
||||||
cy.contains("Calculate").click()
|
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
|
||||||
cy.get(".spectrum-Picker-label").eq(0).click()
|
|
||||||
cy.contains("Statistics").click()
|
|
||||||
|
|
||||||
cy.get(".spectrum-Picker-label").eq(1).click()
|
|
||||||
cy.contains("age").click({ force: true })
|
|
||||||
|
|
||||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
|
||||||
})
|
|
||||||
cy.wait(1000)
|
|
||||||
|
|
||||||
cy.get(".title").then($headers => {
|
|
||||||
expect($headers).to.have.length(7)
|
|
||||||
const headers = Array.from($headers).map(header =>
|
|
||||||
header.textContent.trim()
|
|
||||||
)
|
|
||||||
expect(removeSpacing(headers)).to.deep.eq([
|
|
||||||
"field",
|
|
||||||
"sum",
|
|
||||||
"min",
|
|
||||||
"max",
|
|
||||||
"count",
|
|
||||||
"sumsqr",
|
|
||||||
"avg",
|
|
||||||
])
|
|
||||||
})
|
|
||||||
cy.get(".spectrum-Table-cell").then($values => {
|
|
||||||
let values = Array.from($values).map(header => header.textContent.trim())
|
|
||||||
expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("groups the view by group", () => {
|
|
||||||
cy.contains("Group by").click()
|
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
|
||||||
cy.get(".spectrum-Picker-label").eq(0).click()
|
|
||||||
cy.contains("group").click()
|
|
||||||
cy.contains("Save").click()
|
|
||||||
})
|
|
||||||
cy.wait(1000)
|
|
||||||
cy.contains("Students").should("be.visible")
|
|
||||||
cy.contains("Teachers").should("be.visible")
|
|
||||||
|
|
||||||
cy.get(".spectrum-Table-cell").then($values => {
|
|
||||||
let values = Array.from($values).map(header => header.textContent.trim())
|
|
||||||
expect(values).to.deep.eq([
|
|
||||||
"Students",
|
|
||||||
"70",
|
|
||||||
"20",
|
|
||||||
"25",
|
|
||||||
"3",
|
|
||||||
"1650",
|
|
||||||
"23.333333333333332",
|
|
||||||
"Teachers",
|
|
||||||
"85",
|
|
||||||
"36",
|
|
||||||
"49",
|
|
||||||
"2",
|
|
||||||
"3697",
|
|
||||||
"42.5",
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("renames a view", () => {
|
|
||||||
cy.contains(".nav-item", "Test View")
|
|
||||||
.find(".actions .icon")
|
|
||||||
.click({ force: true })
|
|
||||||
cy.get(".spectrum-Menu-itemLabel").contains("Edit").click()
|
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
|
||||||
cy.get("input").type(" Updated")
|
|
||||||
cy.contains("Save").click()
|
|
||||||
})
|
|
||||||
cy.wait(1000)
|
|
||||||
cy.contains("Test View Updated").should("be.visible")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("deletes a view", () => {
|
|
||||||
cy.contains(".nav-item", "Test View Updated")
|
|
||||||
.find(".actions .icon")
|
|
||||||
.click({ force: true })
|
|
||||||
cy.contains("Delete").click()
|
|
||||||
cy.contains("Delete View").click()
|
|
||||||
cy.wait(500)
|
|
||||||
cy.contains("TestView Updated").should("not.exist")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function removeSpacing(headers) {
|
|
||||||
let newHeaders = []
|
|
||||||
for (let header of headers) {
|
|
||||||
newHeaders.push(header.replace(/\s\s+/g, " "))
|
|
||||||
}
|
}
|
||||||
return newHeaders
|
})
|
||||||
}
|
|
||||||
|
|
|
@ -1,84 +1,87 @@
|
||||||
xcontext("Custom Theming Properties", () => {
|
import filterTests from "../support/filterTests"
|
||||||
before(() => {
|
|
||||||
cy.login()
|
|
||||||
cy.createTestApp()
|
|
||||||
cy.navigateToFrontend()
|
|
||||||
})
|
|
||||||
|
|
||||||
/* Default Values:
|
filterTests(['all'], () => {
|
||||||
Button roundness = Large
|
xcontext("Custom Theming Properties", () => {
|
||||||
Accent colour = Blue 600
|
before(() => {
|
||||||
Accent colour (hover) = Blue 500
|
cy.login()
|
||||||
Navigation bar background colour = Gray 100
|
cy.createTestApp()
|
||||||
Navigation bar text colour = Gray 800 */
|
cy.navigateToFrontend()
|
||||||
it("should reset the color property values", () => {
|
})
|
||||||
// Open Theme modal and change colours
|
|
||||||
cy.get(".spectrum-ActionButton-label").contains("Theme").click()
|
/* Default Values:
|
||||||
cy.get(".spectrum-Picker").contains("Large").click()
|
Button roundness = Large
|
||||||
.parents()
|
Accent colour = Blue 600
|
||||||
.get(".spectrum-Menu-itemLabel").contains("None").click()
|
Accent colour (hover) = Blue 500
|
||||||
changeThemeColors()
|
Navigation bar background colour = Gray 100
|
||||||
// Reset colours
|
Navigation bar text colour = Gray 800 */
|
||||||
cy.get(".spectrum-Button-label").contains("Reset").click({force: true})
|
it("should reset the color property values", () => {
|
||||||
// Check values have reset
|
// Open Theme modal and change colours
|
||||||
checkThemeColorDefaults()
|
cy.get(".spectrum-ActionButton-label").contains("Theme").click()
|
||||||
})
|
cy.get(".spectrum-Picker").contains("Large").click()
|
||||||
|
.parents()
|
||||||
/* Button Roundness Values:
|
.get(".spectrum-Menu-itemLabel").contains("None").click()
|
||||||
None = 0
|
changeThemeColors()
|
||||||
Small = 4px
|
// Reset colours
|
||||||
Medium = 8px
|
cy.get(".spectrum-Button-label").contains("Reset").click({force: true})
|
||||||
Large = 16px */
|
// Check values have reset
|
||||||
it("should test button roundness", () => {
|
checkThemeColorDefaults()
|
||||||
const buttonRoundnessValues = ["0", "4px", "8px", "16px"]
|
})
|
||||||
cy.wait(1000)
|
|
||||||
// Add button, change roundness and confirm value
|
/* Button Roundness Values:
|
||||||
cy.addComponent("Button", null).then((componentId) => {
|
None = 0
|
||||||
buttonRoundnessValues.forEach(function (item, index){
|
Small = 4px
|
||||||
cy.get(".spectrum-ActionButton-label").contains("Theme").click()
|
Medium = 8px
|
||||||
cy.get(".setting").contains("Button roundness").parent()
|
Large = 16px */
|
||||||
.get(".select-wrapper").click()
|
it("should test button roundness", () => {
|
||||||
cy.get(".spectrum-Popover").find('li').eq(index).click()
|
const buttonRoundnessValues = ["0", "4px", "8px", "16px"]
|
||||||
cy.get(".spectrum-Button").contains("View changes").click({force: true})
|
cy.wait(1000)
|
||||||
cy.reload()
|
// Add button, change roundness and confirm value
|
||||||
cy.getComponent(componentId)
|
cy.addComponent("Button", null).then((componentId) => {
|
||||||
.parents(".svelte-xiqd1c").eq(0).should('have.attr', 'style').and('contains', `--buttonBorderRadius:${item}`)
|
buttonRoundnessValues.forEach(function (item, index){
|
||||||
|
cy.get(".spectrum-ActionButton-label").contains("Theme").click()
|
||||||
|
cy.get(".setting").contains("Button roundness").parent()
|
||||||
|
.get(".select-wrapper").click()
|
||||||
|
cy.get(".spectrum-Popover").find('li').eq(index).click()
|
||||||
|
cy.get(".spectrum-Button").contains("View changes").click({force: true})
|
||||||
|
cy.reload()
|
||||||
|
cy.getComponent(componentId)
|
||||||
|
.parents(".svelte-xiqd1c").eq(0).should('have.attr', 'style').and('contains', `--buttonBorderRadius:${item}`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const changeThemeColors = () => {
|
||||||
|
// Changes the theme colours
|
||||||
|
cy.get(".spectrum-FieldLabel").contains("Accent color")
|
||||||
|
.parent().find(".container.svelte-z3cm5a").click()
|
||||||
|
.find('[title="Red 400"]').click()
|
||||||
|
cy.get(".spectrum-FieldLabel").contains("Accent color (hover)")
|
||||||
|
.parent().find(".container.svelte-z3cm5a").click()
|
||||||
|
.find('[title="Orange 400"]').click()
|
||||||
|
cy.get(".spectrum-FieldLabel").contains("Navigation bar background color")
|
||||||
|
.parent().find(".container.svelte-z3cm5a").click()
|
||||||
|
.find('[title="Yellow 400"]').click()
|
||||||
|
cy.get(".spectrum-FieldLabel").contains("Navigation bar text color")
|
||||||
|
.parent().find(".container.svelte-z3cm5a").click()
|
||||||
|
.find('[title="Green 400"]').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkThemeColorDefaults = () => {
|
||||||
|
cy.get(".spectrum-FieldLabel").contains("Accent color")
|
||||||
|
.parent().find(".container.svelte-z3cm5a").click()
|
||||||
|
.get('[title="Blue 600"]').children().find('[aria-label="Checkmark"]')
|
||||||
|
cy.get(".spectrum-Dialog-grid").click()
|
||||||
|
cy.get(".spectrum-FieldLabel").contains("Accent color (hover)")
|
||||||
|
.parent().find(".container.svelte-z3cm5a").click()
|
||||||
|
.get('[title="Blue 500"]').children().find('[aria-label="Checkmark"]')
|
||||||
|
cy.get(".spectrum-Dialog-grid").click()
|
||||||
|
cy.get(".spectrum-FieldLabel").contains("Navigation bar background color")
|
||||||
|
.parent().find(".container.svelte-z3cm5a").click()
|
||||||
|
.get('[title="Gray 100"]').children().find('[aria-label="Checkmark"]')
|
||||||
|
cy.get(".spectrum-Dialog-grid").click()
|
||||||
|
cy.get(".spectrum-FieldLabel").contains("Navigation bar text color")
|
||||||
|
.parent().find(".container.svelte-z3cm5a").click()
|
||||||
|
.get('[title="Gray 800"]').children().find('[aria-label="Checkmark"]')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const changeThemeColors = () => {
|
|
||||||
// Changes the theme colours
|
|
||||||
cy.get(".spectrum-FieldLabel").contains("Accent color")
|
|
||||||
.parent().find(".container.svelte-z3cm5a").click()
|
|
||||||
.find('[title="Red 400"]').click()
|
|
||||||
cy.get(".spectrum-FieldLabel").contains("Accent color (hover)")
|
|
||||||
.parent().find(".container.svelte-z3cm5a").click()
|
|
||||||
.find('[title="Orange 400"]').click()
|
|
||||||
cy.get(".spectrum-FieldLabel").contains("Navigation bar background color")
|
|
||||||
.parent().find(".container.svelte-z3cm5a").click()
|
|
||||||
.find('[title="Yellow 400"]').click()
|
|
||||||
cy.get(".spectrum-FieldLabel").contains("Navigation bar text color")
|
|
||||||
.parent().find(".container.svelte-z3cm5a").click()
|
|
||||||
.find('[title="Green 400"]').click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkThemeColorDefaults = () => {
|
|
||||||
cy.get(".spectrum-FieldLabel").contains("Accent color")
|
|
||||||
.parent().find(".container.svelte-z3cm5a").click()
|
|
||||||
.get('[title="Blue 600"]').children().find('[aria-label="Checkmark"]')
|
|
||||||
cy.get(".spectrum-Dialog-grid").click()
|
|
||||||
cy.get(".spectrum-FieldLabel").contains("Accent color (hover)")
|
|
||||||
.parent().find(".container.svelte-z3cm5a").click()
|
|
||||||
.get('[title="Blue 500"]').children().find('[aria-label="Checkmark"]')
|
|
||||||
cy.get(".spectrum-Dialog-grid").click()
|
|
||||||
cy.get(".spectrum-FieldLabel").contains("Navigation bar background color")
|
|
||||||
.parent().find(".container.svelte-z3cm5a").click()
|
|
||||||
.get('[title="Gray 100"]').children().find('[aria-label="Checkmark"]')
|
|
||||||
cy.get(".spectrum-Dialog-grid").click()
|
|
||||||
cy.get(".spectrum-FieldLabel").contains("Navigation bar text color")
|
|
||||||
.parent().find(".container.svelte-z3cm5a").click()
|
|
||||||
.get('[title="Gray 800"]').children().find('[aria-label="Checkmark"]')
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import filterTests from "../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(['all'], () => {
|
||||||
|
context("Datasource Wizard", () => {
|
||||||
|
if (Cypress.env("TEST_ENV")) {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should navigate in and out of a datasource via wizard", () => {
|
||||||
|
// Select PostgreSQL and add config (without fetch)
|
||||||
|
const datasource = "Oracle"
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.addDatasourceConfig(datasource, true)
|
||||||
|
|
||||||
|
// Navigate back within datasource wizard
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Back").click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Select PostgreSQL datasource again
|
||||||
|
cy.get(".item-list").contains(datasource).click()
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch tables after selection
|
||||||
|
// Previously entered config should not have been saved
|
||||||
|
// Config is back to default values
|
||||||
|
// Modal will close and provide 500 error
|
||||||
|
cy.intercept('**/datasources').as('datasourceConnection')
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Save and fetch tables").click({ force: true })
|
||||||
|
})
|
||||||
|
cy.wait("@datasourceConnection")
|
||||||
|
cy.get("@datasourceConnection").its('response.body')
|
||||||
|
.should('have.property', 'status', 500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,222 @@
|
||||||
|
import filterTests from "../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("MySQL Datasource Testing", () => {
|
||||||
|
if (Cypress.env("TEST_ENV")) {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
const datasource = "MySQL"
|
||||||
|
const queryName = "Cypress Test Query"
|
||||||
|
const queryRename = "CT Query Rename"
|
||||||
|
|
||||||
|
it("Should add MySQL data source without configuration", () => {
|
||||||
|
// Select MySQL data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
// Attempt to fetch tables without applying configuration
|
||||||
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Save and fetch tables")
|
||||||
|
.click({ force: true })
|
||||||
|
// Intercept Request after button click & apply assertions
|
||||||
|
cy.wait("@datasource")
|
||||||
|
cy.get("@datasource")
|
||||||
|
.its("response.body")
|
||||||
|
.should(
|
||||||
|
"have.property",
|
||||||
|
"message",
|
||||||
|
"connect ECONNREFUSED 127.0.0.1:3306"
|
||||||
|
)
|
||||||
|
cy.get("@datasource")
|
||||||
|
.its("response.body")
|
||||||
|
.should("have.property", "status", 500)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add MySQL data source and fetch tables", () => {
|
||||||
|
// Add & configure MySQL data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
cy.addDatasourceConfig(datasource)
|
||||||
|
// Check response from datasource after adding configuration
|
||||||
|
cy.wait("@datasource")
|
||||||
|
cy.get("@datasource").its("response.statusCode").should("eq", 200)
|
||||||
|
// Confirm fetch tables was successful
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("be.gt", 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should check table fetching error", () => {
|
||||||
|
// MySQL test data source contains tables without primary keys
|
||||||
|
cy.get(".spectrum-InLineAlert")
|
||||||
|
.should("contain", "Error fetching tables")
|
||||||
|
.and("contain", "No primary key constraint found")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should define a One relationship type", () => {
|
||||||
|
// Select relationship type & configure
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Define relationship")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Picker").eq(0).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("One").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(1).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGIONS").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(2).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(3).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(4).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||||
|
// Save relationship & reload page
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.reload()
|
||||||
|
})
|
||||||
|
// Confirm table length & column name
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("eq", 1)
|
||||||
|
cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should define a Many relationship type", () => {
|
||||||
|
// Select relationship type & configure
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Define relationship")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Picker").eq(0).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("Many").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(1).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("LOCATIONS").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(2).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGIONS").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(3).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(4).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(5).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||||
|
// Save relationship & reload page
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.reload()
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
// Confirm table length & relationship name
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("eq", 2)
|
||||||
|
cy.get(".spectrum-Table-cell").should(
|
||||||
|
"contain",
|
||||||
|
"LOCATIONS through COUNTRIES → REGIONS"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should delete relationships", () => {
|
||||||
|
// Delete both relationships
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.then(len => {
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Table-row").eq(0).click()
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Delete")
|
||||||
|
.click({ force: true })
|
||||||
|
})
|
||||||
|
cy.reload()
|
||||||
|
}
|
||||||
|
// Confirm relationships no longer exist
|
||||||
|
cy.get(".spectrum-Body").should(
|
||||||
|
"contain",
|
||||||
|
"No relationships configured"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add a query", () => {
|
||||||
|
// Add query
|
||||||
|
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
|
||||||
|
cy.get(".spectrum-Form-item")
|
||||||
|
.eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").type(queryName)
|
||||||
|
})
|
||||||
|
// Insert Query within Fields section
|
||||||
|
cy.get(".CodeMirror textarea")
|
||||||
|
.eq(0)
|
||||||
|
.type("SELECT * FROM books", { force: true })
|
||||||
|
// Intercept query execution
|
||||||
|
cy.intercept("**/queries/preview").as("query")
|
||||||
|
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
cy.wait("@query")
|
||||||
|
// Assert against Status Code & Body
|
||||||
|
cy.get("@query").its("response.statusCode").should("eq", 200)
|
||||||
|
cy.get("@query").its("response.body").should("not.be.empty")
|
||||||
|
// Save query
|
||||||
|
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||||
|
cy.get(".nav-item").should("contain", queryName)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should duplicate a query", () => {
|
||||||
|
// Get last nav item - The query
|
||||||
|
cy.get(".nav-item")
|
||||||
|
.last()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".icon").eq(1).click({ force: true })
|
||||||
|
})
|
||||||
|
// Select and confirm duplication
|
||||||
|
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
||||||
|
cy.get(".nav-item").should("contain", queryName + " (1)")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should edit a query name", () => {
|
||||||
|
// Rename query
|
||||||
|
cy.get(".spectrum-Form-item")
|
||||||
|
.eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").clear().type(queryRename)
|
||||||
|
})
|
||||||
|
// Save query
|
||||||
|
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||||
|
cy.get(".nav-item").should("contain", queryRename)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should delete a query", () => {
|
||||||
|
// Get last nav item - The query
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
cy.get(".nav-item")
|
||||||
|
.last()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".icon").eq(1).click({ force: true })
|
||||||
|
})
|
||||||
|
// Select Delete
|
||||||
|
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Delete Query")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
}
|
||||||
|
// Confirm deletion
|
||||||
|
cy.get(".nav-item").should("not.contain", queryName)
|
||||||
|
cy.get(".nav-item").should("not.contain", queryRename)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,230 @@
|
||||||
|
import filterTests from "../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("Oracle Datasource Testing", () => {
|
||||||
|
if (Cypress.env("TEST_ENV")) {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
const datasource = "Oracle"
|
||||||
|
const queryName = "Cypress Test Query"
|
||||||
|
const queryRename = "CT Query Rename"
|
||||||
|
|
||||||
|
it("Should add Oracle data source and skip table fetch", () => {
|
||||||
|
// Select Oracle data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
// Skip table fetch - no config added
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Skip table fetch")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
// Confirm config contains localhost
|
||||||
|
cy.get(".spectrum-Textfield-input")
|
||||||
|
.eq(1)
|
||||||
|
.should("have.value", "localhost")
|
||||||
|
// Add another Oracle data source, configure & skip table fetch
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.addDatasourceConfig(datasource, true)
|
||||||
|
// Confirm config and no tables
|
||||||
|
cy.get(".spectrum-Textfield-input")
|
||||||
|
.eq(1)
|
||||||
|
.should("have.value", Cypress.env("oracle").HOST)
|
||||||
|
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should add Oracle data source and fetch tables without configuration", () => {
|
||||||
|
// Select Oracle data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
// Attempt to fetch tables without applying configuration
|
||||||
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Save and fetch tables")
|
||||||
|
.click({ force: true })
|
||||||
|
// Intercept Request after button click & apply assertions
|
||||||
|
cy.wait("@datasource")
|
||||||
|
cy.get("@datasource")
|
||||||
|
.its("response.body")
|
||||||
|
.should("have.property", "status", 500)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add Oracle data source and fetch tables", () => {
|
||||||
|
// Add & configure Oracle data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
cy.addDatasourceConfig(datasource)
|
||||||
|
// Check response from datasource after adding configuration
|
||||||
|
cy.wait("@datasource")
|
||||||
|
cy.get("@datasource").its("response.statusCode").should("eq", 200)
|
||||||
|
// Confirm fetch tables was successful
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("be.gt", 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should define a One relationship type", () => {
|
||||||
|
// Select relationship type & configure
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Define relationship")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Picker").eq(0).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("One").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(1).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGIONS").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(2).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(3).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(4).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||||
|
// Save relationship & reload page
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.reload()
|
||||||
|
})
|
||||||
|
// Confirm table length & column name
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("eq", 1)
|
||||||
|
cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should define a Many relationship type", () => {
|
||||||
|
// Select relationship type & configure
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Define relationship")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Picker").eq(0).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("Many").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(1).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("LOCATIONS").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(2).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGIONS").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(3).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(4).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(5).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||||
|
// Save relationship & reload page
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.reload()
|
||||||
|
})
|
||||||
|
// Confirm table length & relationship name
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("eq", 2)
|
||||||
|
cy.get(".spectrum-Table-cell").should(
|
||||||
|
"contain",
|
||||||
|
"LOCATIONS through COUNTRIES → REGIONS"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should delete relationships", () => {
|
||||||
|
// Delete both relationships
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.then(len => {
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Table-row").eq(0).click()
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Delete")
|
||||||
|
.click({ force: true })
|
||||||
|
})
|
||||||
|
cy.reload()
|
||||||
|
}
|
||||||
|
// Confirm relationships no longer exist
|
||||||
|
cy.get(".spectrum-Body").should(
|
||||||
|
"contain",
|
||||||
|
"No relationships configured"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add a query", () => {
|
||||||
|
// Add query
|
||||||
|
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
|
||||||
|
cy.get(".spectrum-Form-item")
|
||||||
|
.eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").type(queryName)
|
||||||
|
})
|
||||||
|
// Insert Query within Fields section
|
||||||
|
cy.get(".CodeMirror textarea")
|
||||||
|
.eq(0)
|
||||||
|
.type("SELECT * FROM JOBS", { force: true })
|
||||||
|
// Intercept query execution
|
||||||
|
cy.intercept("**/queries/preview").as("query")
|
||||||
|
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
cy.wait("@query")
|
||||||
|
// Assert against Status Code & Body
|
||||||
|
cy.get("@query").its("response.statusCode").should("eq", 200)
|
||||||
|
cy.get("@query").its("response.body").should("not.be.empty")
|
||||||
|
// Save query
|
||||||
|
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||||
|
cy.get(".nav-item").should("contain", queryName)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should duplicate a query", () => {
|
||||||
|
// Get query nav item
|
||||||
|
cy.get(".nav-item")
|
||||||
|
.contains(queryName)
|
||||||
|
.parent()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||||
|
})
|
||||||
|
// Select and confirm duplication
|
||||||
|
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
||||||
|
cy.get(".nav-item").should("contain", queryName + " (1)")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should edit a query name", () => {
|
||||||
|
// Rename query
|
||||||
|
cy.get(".spectrum-Form-item")
|
||||||
|
.eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").clear().type(queryRename)
|
||||||
|
})
|
||||||
|
// Save query
|
||||||
|
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||||
|
cy.get(".nav-item").should("contain", queryRename)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should delete a query", () => {
|
||||||
|
// Get query nav item - QueryName
|
||||||
|
cy.get(".nav-item")
|
||||||
|
.contains(queryName)
|
||||||
|
.parent()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Select Delete
|
||||||
|
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Delete Query")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
// Confirm deletion
|
||||||
|
cy.get(".nav-item").should("not.contain", queryName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,285 @@
|
||||||
|
import filterTests from "../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["all"], () => {
|
||||||
|
context("PostgreSQL Datasource Testing", () => {
|
||||||
|
if (Cypress.env("TEST_ENV")) {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
const datasource = "PostgreSQL"
|
||||||
|
const queryName = "Cypress Test Query"
|
||||||
|
const queryRename = "CT Query Rename"
|
||||||
|
|
||||||
|
it("Should add PostgreSQL data source without configuration", () => {
|
||||||
|
// Select PostgreSQL data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
// Attempt to fetch tables without applying configuration
|
||||||
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Save and fetch tables")
|
||||||
|
.click({ force: true })
|
||||||
|
// Intercept Request after button click & apply assertions
|
||||||
|
cy.wait("@datasource")
|
||||||
|
cy.get("@datasource")
|
||||||
|
.its("response.body")
|
||||||
|
.should(
|
||||||
|
"have.property",
|
||||||
|
"message",
|
||||||
|
"connect ECONNREFUSED 127.0.0.1:5432"
|
||||||
|
)
|
||||||
|
cy.get("@datasource")
|
||||||
|
.its("response.body")
|
||||||
|
.should("have.property", "status", 500)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add PostgreSQL data source and fetch tables", () => {
|
||||||
|
// Add & configure PostgreSQL data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
cy.addDatasourceConfig(datasource)
|
||||||
|
// Check response from datasource after adding configuration
|
||||||
|
cy.wait("@datasource")
|
||||||
|
cy.get("@datasource").its("response.statusCode").should("eq", 200)
|
||||||
|
// Confirm fetch tables was successful
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("be.gt", 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should define a One relationship type", () => {
|
||||||
|
// Select relationship type & configure
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Define relationship")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Picker").eq(0).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("One").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(1).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGIONS").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(2).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(3).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(4).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||||
|
// Save relationship & reload page
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.reload()
|
||||||
|
})
|
||||||
|
// Confirm table length & column name
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("eq", 1)
|
||||||
|
cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should define a Many relationship type", () => {
|
||||||
|
// Select relationship type & configure
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Define relationship")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Picker").eq(0).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("Many").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(1).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("LOCATIONS").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(2).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGIONS").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(3).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(4).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
|
||||||
|
cy.get(".spectrum-Picker").eq(5).click()
|
||||||
|
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||||
|
// Save relationship & reload page
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.reload()
|
||||||
|
})
|
||||||
|
// Confirm table length & relationship name
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("eq", 2)
|
||||||
|
cy.get(".spectrum-Table-cell").should(
|
||||||
|
"contain",
|
||||||
|
"LOCATIONS through COUNTRIES → REGIONS"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should delete a relationship", () => {
|
||||||
|
cy.get(".hierarchy-items-container").contains(datasource).click()
|
||||||
|
cy.reload()
|
||||||
|
// Delete one relationship
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Table-row").eq(0).click()
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Delete").click({ force: true })
|
||||||
|
})
|
||||||
|
cy.reload()
|
||||||
|
// Confirm relationship was deleted
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(1)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("eq", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add a query", () => {
|
||||||
|
// Add query
|
||||||
|
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
|
||||||
|
cy.get(".spectrum-Form-item")
|
||||||
|
.eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").type(queryName)
|
||||||
|
})
|
||||||
|
// Insert Query within Fields section
|
||||||
|
cy.get(".CodeMirror textarea")
|
||||||
|
.eq(0)
|
||||||
|
.type("SELECT * FROM books", { force: true })
|
||||||
|
// Intercept query execution
|
||||||
|
cy.intercept("**/queries/preview").as("query")
|
||||||
|
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
cy.wait("@query")
|
||||||
|
// Assert against Status Code & Body
|
||||||
|
cy.get("@query").its("response.statusCode").should("eq", 200)
|
||||||
|
cy.get("@query").its("response.body").should("not.be.empty")
|
||||||
|
// Save query
|
||||||
|
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||||
|
cy.get(".hierarchy-items-container").should("contain", queryName)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should switch to schema with no tables", () => {
|
||||||
|
// Switch Schema - To one without any tables
|
||||||
|
cy.get(".hierarchy-items-container").contains(datasource).click()
|
||||||
|
switchSchema("randomText")
|
||||||
|
|
||||||
|
// No tables displayed
|
||||||
|
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found")
|
||||||
|
|
||||||
|
// Previously created query should be visible
|
||||||
|
cy.get(".spectrum-Table").should("contain", queryName)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should switch schemas", () => {
|
||||||
|
// Switch schema - To one with tables
|
||||||
|
switchSchema("1")
|
||||||
|
|
||||||
|
// Confirm tables exist - Check for specific one
|
||||||
|
cy.get(".spectrum-Table").eq(0).should("contain", "test")
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("eq", 1)
|
||||||
|
|
||||||
|
// Confirm specific table visible within left nav bar
|
||||||
|
cy.get(".hierarchy-items-container").should("contain", "test")
|
||||||
|
|
||||||
|
// Switch back to public schema
|
||||||
|
switchSchema("public")
|
||||||
|
|
||||||
|
// Confirm tables exist - again
|
||||||
|
cy.get(".spectrum-Table").eq(0).should("contain", "REGIONS")
|
||||||
|
cy.get(".spectrum-Table")
|
||||||
|
.eq(0)
|
||||||
|
.find(".spectrum-Table-row")
|
||||||
|
.its("length")
|
||||||
|
.should("be.gt", 1)
|
||||||
|
|
||||||
|
// Confirm specific table visible within left nav bar
|
||||||
|
cy.get(".hierarchy-items-container").should("contain", "REGIONS")
|
||||||
|
|
||||||
|
// No relationships and one query
|
||||||
|
cy.get(".spectrum-Body")
|
||||||
|
.eq(3)
|
||||||
|
.should("contain", "No relationships configured.")
|
||||||
|
cy.get(".spectrum-Table").eq(1).should("contain", queryName)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should duplicate a query", () => {
|
||||||
|
// Get last nav item - The query
|
||||||
|
cy.get(".nav-item")
|
||||||
|
.last()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".icon").eq(1).click({ force: true })
|
||||||
|
})
|
||||||
|
// Select and confirm duplication
|
||||||
|
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
||||||
|
cy.get(".nav-item").should("contain", queryName + " (1)")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should edit a query name", () => {
|
||||||
|
// Access query
|
||||||
|
cy.get(".hierarchy-items-container")
|
||||||
|
.contains(queryName + " (1)")
|
||||||
|
.click()
|
||||||
|
|
||||||
|
// Rename query
|
||||||
|
cy.get(".spectrum-Form-item")
|
||||||
|
.eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").clear().type(queryRename)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Run and Save query
|
||||||
|
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||||
|
cy.get(".nav-item").should("contain", queryRename)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should delete a query", () => {
|
||||||
|
// Get last nav item - The query
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
cy.get(".nav-item")
|
||||||
|
.last()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".icon").eq(1).click({ force: true })
|
||||||
|
})
|
||||||
|
// Select Delete
|
||||||
|
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Delete Query")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
}
|
||||||
|
// Confirm deletion
|
||||||
|
cy.get(".nav-item").should("not.contain", queryName)
|
||||||
|
cy.get(".nav-item").should("not.contain", queryRename)
|
||||||
|
})
|
||||||
|
|
||||||
|
const switchSchema = schema => {
|
||||||
|
// Edit configuration - Change Schema
|
||||||
|
cy.get(".spectrum-Textfield")
|
||||||
|
.eq(6)
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").clear().type(schema)
|
||||||
|
})
|
||||||
|
// Save configuration & fetch
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Fetch tables")
|
||||||
|
.click({ force: true })
|
||||||
|
// Click fetch tables again within modal
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Fetch tables")
|
||||||
|
.click({ force: true })
|
||||||
|
})
|
||||||
|
cy.reload()
|
||||||
|
cy.wait(5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,47 @@
|
||||||
|
import filterTests from "../../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["smoke", "all"], () => {
|
||||||
|
context("REST Datasource Testing", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
const datasource = "REST"
|
||||||
|
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||||
|
|
||||||
|
it("Should add REST data source with incorrect API", () => {
|
||||||
|
// Select REST data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
// Enter incorrect api & attempt to send query
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
|
||||||
|
cy.intercept("**/preview").as("queryError")
|
||||||
|
cy.get("input").clear().type("random text")
|
||||||
|
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
||||||
|
// Intercept Request after button click & apply assertions
|
||||||
|
cy.wait("@queryError")
|
||||||
|
cy.get("@queryError")
|
||||||
|
.its("response.body")
|
||||||
|
.should("have.property", "message", "Invalid URL: http://random text?")
|
||||||
|
cy.get("@queryError")
|
||||||
|
.its("response.body")
|
||||||
|
.should("have.property", "status", 400)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add and configure a REST datasource", () => {
|
||||||
|
// Select REST datasource and create query
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.wait(500)
|
||||||
|
// createRestQuery confirms query creation
|
||||||
|
cy.createRestQuery("GET", restUrl, "/breweries")
|
||||||
|
// Confirm status code response within REST datasource
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".stats").within(() => {
|
||||||
|
cy.get(".spectrum-FieldLabel")
|
||||||
|
.eq(0)
|
||||||
|
.should("contain", 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,140 @@
|
||||||
|
import filterTests from "../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(["smoke", "all"], () => {
|
||||||
|
context("Query Level Transformers", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.deleteApp("Cypress Tests")
|
||||||
|
cy.createApp("Cypress Tests")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should write a transformer function", () => {
|
||||||
|
// Add REST datasource - contains API for breweries
|
||||||
|
const datasource = "REST"
|
||||||
|
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.createRestQuery("GET", restUrl, "/breweries")
|
||||||
|
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
|
||||||
|
// Get Transformer Function from file
|
||||||
|
cy.readFile("cypress/support/queryLevelTransformerFunction.js").then(
|
||||||
|
transformerFunction => {
|
||||||
|
cy.get(".CodeMirror textarea")
|
||||||
|
// Highlight current text and overwrite with file contents
|
||||||
|
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {
|
||||||
|
force: true,
|
||||||
|
})
|
||||||
|
.type(transformerFunction, { parseSpecialCharSequences: false })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Send Query
|
||||||
|
cy.intercept("**/queries/preview").as("query")
|
||||||
|
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
||||||
|
cy.wait("@query")
|
||||||
|
// Assert against Status Code, body, & body rows
|
||||||
|
cy.get("@query").its("response.statusCode").should("eq", 200)
|
||||||
|
cy.get("@query").its("response.body").should("not.be.empty")
|
||||||
|
cy.get("@query").its("response.body.rows").should("not.be.empty")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add data to the previous query", () => {
|
||||||
|
// Add REST datasource - contains API for breweries
|
||||||
|
const datasource = "REST"
|
||||||
|
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.createRestQuery("GET", restUrl, "/breweries")
|
||||||
|
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
|
||||||
|
// Get Transformer Function with Data from file
|
||||||
|
cy.readFile(
|
||||||
|
"cypress/support/queryLevelTransformerFunctionWithData.js"
|
||||||
|
).then(transformerFunction => {
|
||||||
|
//console.log(transformerFunction[1])
|
||||||
|
cy.get(".CodeMirror textarea")
|
||||||
|
// Highlight current text and overwrite with file contents
|
||||||
|
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {
|
||||||
|
force: true,
|
||||||
|
})
|
||||||
|
.type(transformerFunction, { parseSpecialCharSequences: false })
|
||||||
|
})
|
||||||
|
// Send Query
|
||||||
|
cy.intercept("**/queries/preview").as("query")
|
||||||
|
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
||||||
|
cy.wait("@query")
|
||||||
|
// Assert against Status Code, body, & body rows
|
||||||
|
cy.get("@query").its("response.statusCode").should("eq", 200)
|
||||||
|
cy.get("@query").its("response.body").should("not.be.empty")
|
||||||
|
cy.get("@query").its("response.body.rows").should("not.be.empty")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should run an invalid query within the transformer section", () => {
|
||||||
|
// Add REST datasource - contains API for breweries
|
||||||
|
const datasource = "REST"
|
||||||
|
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.createRestQuery("GET", restUrl, "/breweries")
|
||||||
|
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
|
||||||
|
// Clear the code box and add "test"
|
||||||
|
cy.get(".CodeMirror textarea")
|
||||||
|
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {
|
||||||
|
force: true,
|
||||||
|
})
|
||||||
|
.type("test")
|
||||||
|
// Run Query and intercept
|
||||||
|
cy.intercept("**/preview").as("queryError")
|
||||||
|
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
||||||
|
cy.wait("@queryError")
|
||||||
|
cy.wait(500)
|
||||||
|
// Assert against message and status for the query error
|
||||||
|
cy.get("@queryError")
|
||||||
|
.its("response.body")
|
||||||
|
.should("have.property", "message", "test is not defined")
|
||||||
|
cy.get("@queryError")
|
||||||
|
.its("response.body")
|
||||||
|
.should("have.property", "status", 400)
|
||||||
|
})
|
||||||
|
|
||||||
|
xit("should run an invalid query via POST request", () => {
|
||||||
|
// POST request with transformer as null
|
||||||
|
cy.request({
|
||||||
|
method: "POST",
|
||||||
|
url: `${Cypress.config().baseUrl}/api/queries/`,
|
||||||
|
body: {
|
||||||
|
fields: { headers: {}, queryString: null, path: null },
|
||||||
|
parameters: [],
|
||||||
|
schema: {},
|
||||||
|
name: "test",
|
||||||
|
queryVerb: "read",
|
||||||
|
transformer: null,
|
||||||
|
datasourceId: "test",
|
||||||
|
},
|
||||||
|
// Expected 400 error - Transformer must be a string
|
||||||
|
failOnStatusCode: false,
|
||||||
|
}).then(response => {
|
||||||
|
expect(response.status).to.equal(400)
|
||||||
|
expect(response.body.message).to.include(
|
||||||
|
'Invalid body - "transformer" must be a string'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
xit("should run an empty query", () => {
|
||||||
|
// POST request with Transformer as an empty string
|
||||||
|
cy.request({
|
||||||
|
method: "POST",
|
||||||
|
url: `${Cypress.config().baseUrl}/api/queries/preview`,
|
||||||
|
body: {
|
||||||
|
fields: { headers: {}, queryString: null, path: null },
|
||||||
|
queryVerb: "read",
|
||||||
|
transformer: "",
|
||||||
|
datasourceId: "test",
|
||||||
|
},
|
||||||
|
// Expected 400 error - Transformer is not allowed to be empty
|
||||||
|
failOnStatusCode: false,
|
||||||
|
}).then(response => {
|
||||||
|
expect(response.status).to.equal(400)
|
||||||
|
expect(response.body.message).to.include(
|
||||||
|
'Invalid body - "transformer" is not allowed to be empty'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,103 +1,133 @@
|
||||||
context("Rename an App", () => {
|
import filterTests from "../support/filterTests"
|
||||||
beforeEach(() => {
|
|
||||||
cy.login()
|
|
||||||
cy.createTestApp()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should rename an unpublished application", () => {
|
filterTests(['all'], () => {
|
||||||
const appRename = "Cypress Renamed"
|
context("Rename an App", () => {
|
||||||
// Rename app, Search for app, Confirm name was changed
|
beforeEach(() => {
|
||||||
cy.get(".home-logo").click()
|
cy.login()
|
||||||
renameApp(appRename)
|
cy.createTestApp()
|
||||||
cy.searchForApplication(appRename)
|
|
||||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
|
||||||
cy.deleteApp(appRename)
|
|
||||||
})
|
|
||||||
|
|
||||||
xit("Should rename a published application", () => {
|
|
||||||
// It is not possible to rename a published application
|
|
||||||
const appRename = "Cypress Renamed"
|
|
||||||
// Publish the app
|
|
||||||
cy.get(".toprightnav")
|
|
||||||
cy.get(".spectrum-Button").contains("Publish").click({force: true})
|
|
||||||
cy.get(".spectrum-Dialog-grid")
|
|
||||||
.within(() => {
|
|
||||||
// Click publish again within the modal
|
|
||||||
cy.get(".spectrum-Button").contains("Publish").click({force: true})
|
|
||||||
})
|
})
|
||||||
// Rename app, Search for app, Confirm name was changed
|
|
||||||
cy.get(".home-logo").click()
|
|
||||||
renameApp(appRename, true)
|
|
||||||
cy.searchForApplication(appRename)
|
|
||||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Should try to rename an application to have no name", () => {
|
it("should rename an unpublished application", () => {
|
||||||
cy.get(".home-logo").click()
|
const appName = "Cypress Tests"
|
||||||
renameApp(" ", false, true)
|
const appRename = "Cypress Renamed"
|
||||||
// Close modal and confirm name has not been changed
|
// Rename app, Search for app, Confirm name was changed
|
||||||
cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
|
cy.get(".home-logo").click()
|
||||||
cy.searchForApplication("Cypress Tests")
|
renameApp(appName, appRename)
|
||||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
cy.reload()
|
||||||
})
|
cy.wait(1000)
|
||||||
|
cy.searchForApplication(appRename)
|
||||||
xit("Should create two applications with the same name", () => {
|
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||||
// It is not possible to have applications with the same name
|
// Set app name back to Cypress Tests
|
||||||
const appName = "Cypress Tests"
|
cy.reload()
|
||||||
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
|
cy.wait(1000)
|
||||||
cy.wait(500)
|
renameApp(appRename, appName)
|
||||||
cy.get(".spectrum-Button").contains("Create app").click({force: true})
|
|
||||||
cy.contains(/Start from scratch/).click()
|
|
||||||
cy.get(".spectrum-Modal")
|
|
||||||
.within(() => {
|
|
||||||
cy.get("input").eq(0).type(appName)
|
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true})
|
|
||||||
cy.get(".error").should("have.text", "Another app with the same name already exists")
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
xit("Should rename a published application", () => {
|
||||||
it("should validate application names", () => {
|
// It is not possible to rename a published application
|
||||||
// App name must be letters, numbers and spaces only
|
const appName = "Cypress Tests"
|
||||||
// This test checks numbers and special characters specifically
|
const appRename = "Cypress Renamed"
|
||||||
const numberName = 12345
|
// Publish the app
|
||||||
const specialCharName = "£$%^"
|
cy.get(".toprightnav")
|
||||||
cy.get(".home-logo").click()
|
cy.get(".spectrum-Button").contains("Publish").click({force: true})
|
||||||
renameApp(numberName)
|
cy.get(".spectrum-Dialog-grid")
|
||||||
cy.searchForApplication(numberName)
|
|
||||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
|
||||||
renameApp(specialCharName)
|
|
||||||
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
|
|
||||||
})
|
|
||||||
|
|
||||||
const renameApp = (appName, published, noName) => {
|
|
||||||
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`)
|
|
||||||
.its("body")
|
|
||||||
.then(val => {
|
|
||||||
if (val.length > 0) {
|
|
||||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
|
||||||
// Check for when an app is published
|
|
||||||
if (published == true){
|
|
||||||
// Should not have Edit as option, will unpublish app
|
|
||||||
cy.should("not.have.value", "Edit")
|
|
||||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
|
||||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
|
||||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
|
||||||
}
|
|
||||||
cy.contains("Edit").click()
|
|
||||||
cy.get(".spectrum-Modal")
|
|
||||||
.within(() => {
|
.within(() => {
|
||||||
if (noName == true){
|
// Click publish again within the modal
|
||||||
cy.get("input").clear()
|
cy.get(".spectrum-Button").contains("Publish").click({force: true})
|
||||||
cy.get(".spectrum-Dialog-grid").click()
|
})
|
||||||
.contains("App name must be letters, numbers and spaces only")
|
// Rename app, Search for app, Confirm name was changed
|
||||||
return cy
|
cy.get(".home-logo").click()
|
||||||
|
renameApp(appName, appRename, true)
|
||||||
|
cy.searchForApplication(appRename)
|
||||||
|
cy.get(".appTable").find(".wrapper").should("have.length", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should try to rename an application to have no name", () => {
|
||||||
|
const appName = "Cypress Tests"
|
||||||
|
cy.get(".home-logo").click()
|
||||||
|
renameApp(appName, " ", false, true)
|
||||||
|
cy.wait(500)
|
||||||
|
// Close modal and confirm name has not been changed
|
||||||
|
cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
|
||||||
|
cy.reload()
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.searchForApplication(appName)
|
||||||
|
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
xit("Should create two applications with the same name", () => {
|
||||||
|
// It is not possible to have applications with the same name
|
||||||
|
const appName = "Cypress Tests"
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Button").contains("Create app").click({force: true})
|
||||||
|
cy.contains(/Start from scratch/).click()
|
||||||
|
cy.get(".spectrum-Modal")
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").eq(0).type(appName)
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true})
|
||||||
|
cy.get(".error").should("have.text", "Another app with the same name already exists")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should validate application names", () => {
|
||||||
|
// App name must be letters, numbers and spaces only
|
||||||
|
// This test checks numbers and special characters specifically
|
||||||
|
const appName = "Cypress Tests"
|
||||||
|
const numberName = 12345
|
||||||
|
const specialCharName = "£$%^"
|
||||||
|
cy.get(".home-logo").click()
|
||||||
|
renameApp(appName, numberName)
|
||||||
|
cy.reload()
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.searchForApplication(numberName)
|
||||||
|
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||||
|
cy.reload()
|
||||||
|
cy.wait(1000)
|
||||||
|
renameApp(numberName, specialCharName)
|
||||||
|
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
|
||||||
|
// Set app name back to Cypress Tests
|
||||||
|
cy.reload()
|
||||||
|
cy.wait(1000)
|
||||||
|
renameApp(numberName, appName)
|
||||||
|
})
|
||||||
|
|
||||||
|
const renameApp = (originalName, changedName, published, noName) => {
|
||||||
|
cy.searchForApplication(originalName)
|
||||||
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
|
.its("body")
|
||||||
|
.then(val => {
|
||||||
|
if (val.length > 0) {
|
||||||
|
cy.get(".appTable")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(1).click()
|
||||||
|
})
|
||||||
|
// Check for when an app is published
|
||||||
|
if (published == true){
|
||||||
|
// Should not have Edit as option, will unpublish app
|
||||||
|
cy.should("not.have.value", "Edit")
|
||||||
|
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||||
|
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||||
|
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
||||||
}
|
}
|
||||||
cy.get("input").clear()
|
cy.contains("Edit").click()
|
||||||
cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
|
cy.get(".spectrum-Modal")
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true})
|
.within(() => {
|
||||||
cy.wait(500)
|
if (noName == true){
|
||||||
})
|
cy.get("input").clear()
|
||||||
|
cy.get(".spectrum-Dialog-grid").click()
|
||||||
|
.contains("App name must be letters, numbers and spaces only")
|
||||||
|
return cy
|
||||||
|
}
|
||||||
|
cy.get("input").clear()
|
||||||
|
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true})
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import filterTests from "../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(['smoke', 'all'], () => {
|
||||||
|
context("Revert apps", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should try to revert an unpublished app", () => {
|
||||||
|
// Click revert icon
|
||||||
|
cy.get(".toprightnav").within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(1).click()
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
// Enter app name before revert
|
||||||
|
cy.get("input").type("Cypress Tests")
|
||||||
|
cy.intercept('**/revert').as('revertApp')
|
||||||
|
// Click Revert
|
||||||
|
cy.get(".spectrum-Button").contains("Revert").click({ force: true })
|
||||||
|
// Intercept Request after button click & apply assertions
|
||||||
|
cy.wait("@revertApp")
|
||||||
|
cy.get("@revertApp").its('response.body').should('have.property', 'message', "App has not yet been deployed")
|
||||||
|
cy.get("@revertApp").its('response.body').should('have.property', 'status', 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should revert a published app", () => {
|
||||||
|
// Add initial component - Paragraph
|
||||||
|
cy.addComponent("Elements", "Paragraph")
|
||||||
|
// Publish app
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||||
|
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||||
|
})
|
||||||
|
// Add second component - Button
|
||||||
|
cy.addComponent("Elements", "Button")
|
||||||
|
// Click Revert
|
||||||
|
cy.get(".toprightnav").within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(1).click()
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
// Click Revert
|
||||||
|
cy.get(".spectrum-Button").contains("Revert").click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
// Confirm Paragraph component is still visible
|
||||||
|
cy.get(".root").contains("New Paragraph")
|
||||||
|
// Confirm Button component is not visible
|
||||||
|
cy.get(".root").should("not.have.text", "New Button")
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should enter incorrect app name when reverting", () => {
|
||||||
|
// Click Revert
|
||||||
|
cy.get(".toprightnav").within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||||
|
})
|
||||||
|
// Enter incorrect app name
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get("input").type("Cypress Tests")
|
||||||
|
// Revert button within modal should be disabled
|
||||||
|
cy.get(".spectrum-Button").eq(1).should('be.disabled')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -4,17 +4,17 @@ const path = require("path")
|
||||||
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
||||||
|
|
||||||
// normal development system
|
// normal development system
|
||||||
const WORKER_PORT = "10002"
|
const SERVER_PORT = cypressConfig.env.PORT
|
||||||
const MAIN_PORT = cypressConfig.env.PORT
|
const WORKER_PORT = cypressConfig.env.WORKER_PORT
|
||||||
|
|
||||||
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
||||||
process.env.NODE_ENV = "cypress"
|
process.env.NODE_ENV = "cypress"
|
||||||
process.env.ENABLE_ANALYTICS = "false"
|
process.env.ENABLE_ANALYTICS = "false"
|
||||||
process.env.PORT = MAIN_PORT
|
|
||||||
process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET
|
process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET
|
||||||
process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
|
process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
|
||||||
process.env.SELF_HOSTED = 1
|
process.env.SELF_HOSTED = 1
|
||||||
process.env.WORKER_URL = "http://localhost:10002/"
|
process.env.WORKER_URL = `http://localhost:${WORKER_PORT}/`
|
||||||
process.env.APPS_URL = `http://localhost:${MAIN_PORT}/`
|
process.env.APPS_URL = `http://localhost:${SERVER_PORT}/`
|
||||||
process.env.MINIO_URL = `http://localhost:4004`
|
process.env.MINIO_URL = `http://localhost:4004`
|
||||||
process.env.MINIO_ACCESS_KEY = "budibase"
|
process.env.MINIO_ACCESS_KEY = "budibase"
|
||||||
process.env.MINIO_SECRET_KEY = "budibase"
|
process.env.MINIO_SECRET_KEY = "budibase"
|
||||||
|
@ -33,11 +33,14 @@ exports.run = (
|
||||||
// require("dotenv").config({ path: resolve(dir, ".env") })
|
// require("dotenv").config({ path: resolve(dir, ".env") })
|
||||||
// don't make this a variable or top level require
|
// don't make this a variable or top level require
|
||||||
// it will cause environment module to be loaded prematurely
|
// it will cause environment module to be loaded prematurely
|
||||||
require(serverLoc)
|
|
||||||
|
// override the port with the worker port temporarily
|
||||||
process.env.PORT = WORKER_PORT
|
process.env.PORT = WORKER_PORT
|
||||||
require(workerLoc)
|
require(workerLoc)
|
||||||
// reload main port for rest of system
|
|
||||||
process.env.PORT = MAIN_PORT
|
// override the port with the server port
|
||||||
|
process.env.PORT = SERVER_PORT
|
||||||
|
require(serverLoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
|
|
@ -10,7 +10,7 @@ Cypress.on("uncaught:exception", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("login", () => {
|
Cypress.Commands.add("login", () => {
|
||||||
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(2000)
|
cy.wait(2000)
|
||||||
cy.url().then(url => {
|
cy.url().then(url => {
|
||||||
if (url.includes("builder/admin")) {
|
if (url.includes("builder/admin")) {
|
||||||
|
@ -33,37 +33,69 @@ Cypress.Commands.add("login", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createApp", name => {
|
Cypress.Commands.add("createApp", name => {
|
||||||
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.request(`${Cypress.config().baseUrl}api/applications?status=all`)
|
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
|
||||||
.its("body")
|
|
||||||
.then(body => {
|
|
||||||
if (body.length > 0) {
|
|
||||||
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||||
cy.wait(7000)
|
cy.wait(10000)
|
||||||
})
|
})
|
||||||
|
cy.createTable("Cypress Tests", true)
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("deleteApp", appName => {
|
Cypress.Commands.add("deleteApp", name => {
|
||||||
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(1000)
|
cy.wait(2000)
|
||||||
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`)
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
.its("body")
|
.its("body")
|
||||||
.then(val => {
|
.then(val => {
|
||||||
if (val.length > 0) {
|
if (val.length > 0) {
|
||||||
cy.get(
|
cy.searchForApplication(name)
|
||||||
".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon"
|
cy.get(".appTable").within(() => {
|
||||||
).click()
|
cy.get(".spectrum-Icon").eq(1).click()
|
||||||
cy.contains("Delete").click()
|
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
|
||||||
cy.get("input").type(appName)
|
|
||||||
cy.get(".spectrum-Button--warning").click()
|
|
||||||
})
|
})
|
||||||
|
cy.get(".spectrum-Menu").then($menu => {
|
||||||
|
if ($menu.text().includes("Unpublish")) {
|
||||||
|
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||||
|
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||||
|
} else {
|
||||||
|
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get("input").type(name)
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Button--warning").click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("deleteAllApps", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(500)
|
||||||
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
|
.its("body")
|
||||||
|
.then(val => {
|
||||||
|
for (let i = 0; i < val.length; i++) {
|
||||||
|
cy.get(".spectrum-Heading")
|
||||||
|
.eq(1)
|
||||||
|
.then(app => {
|
||||||
|
const name = app.text()
|
||||||
|
cy.get(".title")
|
||||||
|
.children()
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(0).click()
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get("input").type(name)
|
||||||
|
cy.get(".spectrum-Button--warning").click()
|
||||||
|
})
|
||||||
|
cy.reload()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -72,6 +104,7 @@ Cypress.Commands.add("createTestApp", () => {
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
cy.deleteApp(appName)
|
cy.deleteApp(appName)
|
||||||
cy.createApp(appName, "This app is used for Cypress testing.")
|
cy.createApp(appName, "This app is used for Cypress testing.")
|
||||||
|
cy.createScreen("home", "home")
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createTestTableWithData", () => {
|
Cypress.Commands.add("createTestTableWithData", () => {
|
||||||
|
@ -80,10 +113,18 @@ Cypress.Commands.add("createTestTableWithData", () => {
|
||||||
cy.addColumn("dog", "age", "Number")
|
cy.addColumn("dog", "age", "Number")
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createTable", tableName => {
|
Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
||||||
cy.contains("Budibase DB").click()
|
if (!initialTable) {
|
||||||
cy.contains("Create new table").click()
|
cy.navigateToDataSection()
|
||||||
|
cy.get(`[data-cy="new-table"]`).click()
|
||||||
|
}
|
||||||
|
cy.wait(5000)
|
||||||
|
cy.get(".spectrum-Dialog-grid")
|
||||||
|
.contains("Budibase DB")
|
||||||
|
.click({ force: true })
|
||||||
|
.then(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
|
})
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
cy.get("input").first().type(tableName).blur()
|
cy.get("input").first().type(tableName).blur()
|
||||||
|
@ -131,17 +172,19 @@ Cypress.Commands.add("addRow", values => {
|
||||||
|
|
||||||
Cypress.Commands.add("addRowMultiValue", values => {
|
Cypress.Commands.add("addRowMultiValue", values => {
|
||||||
cy.contains("Create row").click()
|
cy.contains("Create row").click()
|
||||||
cy.get(".spectrum-Form-itemField")
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
.click()
|
cy.get(".spectrum-Form-itemField")
|
||||||
.then(() => {
|
.click()
|
||||||
cy.get(".spectrum-Popover").within(() => {
|
.then(() => {
|
||||||
for (let i = 0; i < values.length; i++) {
|
cy.get(".spectrum-Popover").within(() => {
|
||||||
cy.get(".spectrum-Menu-item").eq(i).click()
|
for (let i = 0; i < values.length; i++) {
|
||||||
}
|
cy.get(".spectrum-Menu-item").eq(i).click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Dialog-grid").click("top")
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").click("top")
|
})
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createUser", email => {
|
Cypress.Commands.add("createUser", email => {
|
||||||
|
@ -190,22 +233,49 @@ Cypress.Commands.add("navigateToFrontend", () => {
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
cy.contains("Design").click()
|
cy.contains("Design").click()
|
||||||
cy.get(".spectrum-Search").type("/")
|
cy.get(".spectrum-Search").type("/")
|
||||||
cy.createScreen("home", "home")
|
|
||||||
cy.addComponent("Elements", "Headline")
|
|
||||||
cy.get(".nav-item").contains("home").click()
|
cy.get(".nav-item").contains("home").click()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("navigateToDataSection", () => {
|
||||||
|
// Clicks on the Data tab
|
||||||
|
cy.wait(500)
|
||||||
|
cy.contains("Data").click()
|
||||||
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createScreen", (screenName, route) => {
|
Cypress.Commands.add("createScreen", (screenName, route) => {
|
||||||
|
cy.contains("Design").click()
|
||||||
cy.get("[aria-label=AddCircle]").click()
|
cy.get("[aria-label=AddCircle]").click()
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get(".item").first().click()
|
cy.get(".item").contains("Blank").click()
|
||||||
cy.get(".spectrum-Button--cta").click()
|
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
})
|
})
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Form-itemField").eq(0).type(screenName)
|
||||||
|
cy.get(".spectrum-Form-itemField").eq(1).type(route)
|
||||||
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("createAutogeneratedScreens", screenNames => {
|
||||||
|
// Screen name must already exist within data source
|
||||||
|
cy.contains("Design").click()
|
||||||
|
cy.get("[aria-label=AddCircle]").click()
|
||||||
|
for (let i = 0; i < screenNames.length; i++) {
|
||||||
|
cy.get(".item").contains(screenNames[i]).click()
|
||||||
|
}
|
||||||
|
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
||||||
|
cy.wait(4000)
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("addRow", values => {
|
||||||
|
cy.contains("Create row").click()
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get("input").first().clear().type(screenName)
|
for (let i = 0; i < values.length; i++) {
|
||||||
cy.get("input").eq(1).clear().type(route)
|
cy.get("input").eq(i).type(values[i]).blur()
|
||||||
cy.get(".spectrum-Button--cta").click()
|
}
|
||||||
cy.wait(2000)
|
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -243,7 +313,144 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("searchForApplication", appName => {
|
Cypress.Commands.add("searchForApplication", appName => {
|
||||||
cy.get(".spectrum-Textfield").within(() => {
|
cy.wait(1000)
|
||||||
cy.get("input").eq(0).type(appName)
|
// Searches for the app
|
||||||
|
cy.get(".filter").then(() => {
|
||||||
|
cy.get(".spectrum-Textfield").within(() => {
|
||||||
|
cy.get("input").eq(0).type(appName)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Confirms app exists after search
|
||||||
|
cy.get(".appTable").contains(appName)
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("selectExternalDatasource", datasourceName => {
|
||||||
|
// Navigates to Data Section
|
||||||
|
cy.navigateToDataSection()
|
||||||
|
// Open Data Source modal
|
||||||
|
cy.get(".nav").within(() => {
|
||||||
|
cy.get(".add-button").click()
|
||||||
|
})
|
||||||
|
// Clicks specified datasource & continue
|
||||||
|
cy.get(".item-list").contains(datasourceName).click()
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => {
|
||||||
|
// selectExternalDatasource should be called prior to this
|
||||||
|
// Adds the config for specified datasource & fetches tables
|
||||||
|
// Currently supports MySQL, PostgreSQL, Oracle
|
||||||
|
// Host IP Address
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".form-row")
|
||||||
|
.eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Textfield").within(() => {
|
||||||
|
if (datasource == "Oracle") {
|
||||||
|
cy.get("input").clear().type(Cypress.env("oracle").HOST)
|
||||||
|
} else {
|
||||||
|
cy.get("input").clear().type(Cypress.env("HOST_IP"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Database Name
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
if (datasource == "MySQL") {
|
||||||
|
cy.get(".form-row")
|
||||||
|
.eq(4)
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").clear().type(Cypress.env("mysql").DATABASE)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cy.get(".form-row")
|
||||||
|
.eq(2)
|
||||||
|
.within(() => {
|
||||||
|
if (datasource == "PostgreSQL") {
|
||||||
|
cy.get("input").clear().type(Cypress.env("postgresql").DATABASE)
|
||||||
|
}
|
||||||
|
if (datasource == "Oracle") {
|
||||||
|
cy.get("input").clear().type(Cypress.env("oracle").DATABASE)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// User
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
if (datasource == "MySQL") {
|
||||||
|
cy.get(".form-row")
|
||||||
|
.eq(2)
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").clear().type(Cypress.env("mysql").USER)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cy.get(".form-row")
|
||||||
|
.eq(3)
|
||||||
|
.within(() => {
|
||||||
|
if (datasource == "PostgreSQL") {
|
||||||
|
cy.get("input").clear().type(Cypress.env("postgresql").USER)
|
||||||
|
}
|
||||||
|
if (datasource == "Oracle") {
|
||||||
|
cy.get("input").clear().type(Cypress.env("oracle").USER)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Password
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
if (datasource == "MySQL") {
|
||||||
|
cy.get(".form-row")
|
||||||
|
.eq(3)
|
||||||
|
.within(() => {
|
||||||
|
cy.get("input").clear().type(Cypress.env("mysql").PASSWORD)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cy.get(".form-row")
|
||||||
|
.eq(4)
|
||||||
|
.within(() => {
|
||||||
|
if (datasource == "PostgreSQL") {
|
||||||
|
cy.get("input").clear().type(Cypress.env("postgresql").PASSWORD)
|
||||||
|
}
|
||||||
|
if (datasource == "Oracle") {
|
||||||
|
cy.get("input").clear().type(Cypress.env("oracle").PASSWORD)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Click to fetch tables
|
||||||
|
if (skipFetch) {
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Skip table fetch")
|
||||||
|
.click({ force: true })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get(".spectrum-Button")
|
||||||
|
.contains("Save and fetch tables")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => {
|
||||||
|
// addExternalDatasource should be called prior to this
|
||||||
|
// Configures REST datasource & sends query
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
|
||||||
|
// Select Method & add Rest URL
|
||||||
|
cy.get(".spectrum-Picker-label").eq(1).click()
|
||||||
|
cy.get(".spectrum-Menu").contains(method).click()
|
||||||
|
cy.get("input").clear().type(restUrl)
|
||||||
|
// Send query
|
||||||
|
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
cy.get(".hierarchy-items-container")
|
||||||
|
.should("contain", method)
|
||||||
|
.and("contain", queryPrettyName)
|
||||||
|
})
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue