diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index e940e6fa10..6e886f3011 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -7,6 +7,7 @@ on: branches: - master - develop + - new-design-ui pull_request: branches: - master @@ -59,3 +60,19 @@ jobs: with: install: false command: yarn test:e2e:ci + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-1 + + - name: Upload to S3 + if: github.ref == 'refs/heads/new-design-ui' + run: | + tar -czvf new_ui.tar.gz packages/server/builder/assets packages/server/builder/index.html + aws s3 cp new_ui.tar.gz s3://prod-budi-app-assets/beta:design_ui/ + aws s3 cp packages/client/dist/budibase-client.js s3://prod-budi-app-assets/beta:design_ui/budibase-client.js + aws cloudfront create-invalidation --distribution-id E3ELKP4RCEHVLW --paths "/beta:design_ui/*" + diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index 4502482b23..631308d945 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -21,7 +21,8 @@ env: # Posthog token used by ui at build time POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} - PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + FEATURE_PREVIEW_URL: https://budirelease.live jobs: release: @@ -124,4 +125,4 @@ jobs: with: webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env." - embed-title: ${{ env.RELEASE_VERSION }} \ No newline at end of file + embed-title: ${{ env.RELEASE_VERSION }} diff --git a/.vscode/settings.json b/.vscode/settings.json index d471924fe0..4838a4fd89 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,17 @@ "editor.codeActionsOnSave": { "source.fixAll": true }, - "editor.defaultFormatter": "svelte.svelte-vscode" + "editor.defaultFormatter": "svelte.svelte-vscode", + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "debug.javascript.terminalOptions": { + "skipFiles": [ + "${workspaceFolder}/packages/backend-core/node_modules/**", + "/**" + ] + }, } diff --git a/charts/budibase/Chart.yaml b/charts/budibase/Chart.yaml index 227a515432..570aa04d8e 100644 --- a/charts/budibase/Chart.yaml +++ b/charts/budibase/Chart.yaml @@ -11,8 +11,8 @@ sources: - https://github.com/Budibase/budibase - https://budibase.com type: application -version: 0.2.10 -appVersion: 1.0.48 +version: 0.2.11 +appVersion: 1.0.214 dependencies: - name: couchdb version: 3.6.1 diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 455d3251a8..2734202fff 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -103,7 +103,7 @@ globals: google: clientId: "" secret: "" - automationMaxIterations: "500" + automationMaxIterations: "200" createSecrets: true # creates an internal API key, JWT secrets and redis password for you diff --git a/lerna.json b/lerna.json index cb7bd2c0f9..c4e57cdc6a 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.212-alpha.0", + "version": "1.0.219-alpha.0", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 8217599f77..69c901495d 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.0.212-alpha.0", + "version": "1.0.219-alpha.0", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -20,7 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "^1.0.212-alpha.0", + "@budibase/types": "^1.0.219-alpha.0", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", "bcrypt": "5.0.1", @@ -36,6 +36,7 @@ "passport-google-oauth": "2.0.0", "passport-jwt": "4.0.0", "passport-local": "1.0.0", + "passport-oauth2-refresh": "^2.1.0", "posthog-node": "1.3.0", "pouchdb": "7.3.0", "pouchdb-find": "7.2.2", diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js index b13cd932c6..b6d6a2027f 100644 --- a/packages/backend-core/src/auth.js +++ b/packages/backend-core/src/auth.js @@ -2,6 +2,9 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy const { getGlobalDB } = require("./tenancy") +const refresh = require("passport-oauth2-refresh") +const { Configs } = require("./constants") +const { getScopedConfig } = require("./db/utils") const { jwt, local, @@ -12,6 +15,7 @@ const { tenancy, appTenancy, authError, + ssoCallbackUrl, csrf, internalApi, } = require("./middleware") @@ -34,6 +38,122 @@ passport.deserializeUser(async (user, done) => { } }) +async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) { + const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig) + let enrichedConfig + let strategy + + try { + enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl) + if (!enrichedConfig) { + throw new Error("OIDC Config contents invalid") + } + strategy = await oidc.strategyFactory(enrichedConfig) + } catch (err) { + console.error(err) + throw new Error("Could not refresh OAuth Token") + } + + refresh.use(strategy, { + setRefreshOAuth2() { + return strategy._getOAuth2Client(enrichedConfig) + }, + }) + + return new Promise(resolve => { + refresh.requestNewAccessToken( + Configs.OIDC, + refreshToken, + (err, accessToken, refreshToken, params) => { + resolve({ err, accessToken, refreshToken, params }) + } + ) + }) +} + +async function refreshGoogleAccessToken(db, config, refreshToken) { + let callbackUrl = await google.getCallbackUrl(db, config) + + let strategy + try { + strategy = await google.strategyFactory(config, callbackUrl) + } catch (err) { + console.error(err) + throw new Error("Error constructing OIDC refresh strategy", err) + } + + refresh.use(strategy) + + return new Promise(resolve => { + refresh.requestNewAccessToken( + Configs.GOOGLE, + refreshToken, + (err, accessToken, refreshToken, params) => { + resolve({ err, accessToken, refreshToken, params }) + } + ) + }) +} + +async function refreshOAuthToken(refreshToken, configType, configId) { + const db = getGlobalDB() + + const config = await getScopedConfig(db, { + type: configType, + group: {}, + }) + + let chosenConfig = {} + let refreshResponse + if (configType === Configs.OIDC) { + // configId - retrieved from cookie. + chosenConfig = config.configs.filter(c => c.uuid === configId)[0] + if (!chosenConfig) { + throw new Error("Invalid OIDC configuration") + } + refreshResponse = await refreshOIDCAccessToken( + db, + chosenConfig, + refreshToken + ) + } else { + chosenConfig = config + refreshResponse = await refreshGoogleAccessToken( + db, + chosenConfig, + refreshToken + ) + } + + return refreshResponse +} + +async function updateUserOAuth(userId, oAuthConfig) { + const details = { + accessToken: oAuthConfig.accessToken, + refreshToken: oAuthConfig.refreshToken, + } + + try { + const db = getGlobalDB() + const dbUser = await db.get(userId) + + //Do not overwrite the refresh token if a valid one is not provided. + if (typeof details.refreshToken !== "string") { + delete details.refreshToken + } + + dbUser.oauth2 = { + ...dbUser.oauth2, + ...details, + } + + await db.put(dbUser) + } catch (e) { + console.error("Could not update OAuth details for current user", e) + } +} + module.exports = { buildAuthMiddleware: authenticated, passport, @@ -46,4 +166,7 @@ module.exports = { authError, buildCsrfMiddleware: csrf, internalApi, + refreshOAuthToken, + updateUserOAuth, + ssoCallbackUrl, } diff --git a/packages/backend-core/src/context/index.js b/packages/backend-core/src/context/index.js index a69a381f0f..bd4d857ef2 100644 --- a/packages/backend-core/src/context/index.js +++ b/packages/backend-core/src/context/index.js @@ -314,6 +314,7 @@ function getContextDB(key, opts) { toUseAppId = getDevelopmentAppID(appId) break } + db = dangerousGetDB(toUseAppId, opts) try { cls.setOnContext(key, db) diff --git a/packages/backend-core/src/db/constants.js b/packages/backend-core/src/db/constants.js deleted file mode 100644 index 10c6e174d7..0000000000 --- a/packages/backend-core/src/db/constants.js +++ /dev/null @@ -1,41 +0,0 @@ -exports.SEPARATOR = "_" - -const PRE_APP = "app" -const PRE_DEV = "dev" - -exports.DocumentTypes = { - USER: "us", - WORKSPACE: "workspace", - CONFIG: "config", - TEMPLATE: "template", - APP: PRE_APP, - DEV: PRE_DEV, - APP_DEV: `${PRE_APP}${exports.SEPARATOR}${PRE_DEV}`, - APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`, - ROLE: "role", - MIGRATIONS: "migrations", - DEV_INFO: "devinfo", -} - -exports.StaticDatabases = { - GLOBAL: { - name: "global-db", - docs: { - apiKeys: "apikeys", - usageQuota: "usage_quota", - licenseInfo: "license_info", - }, - }, - // contains information about tenancy and so on - PLATFORM_INFO: { - name: "global-info", - docs: { - tenants: "tenants", - install: "install", - }, - }, -} - -exports.APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR -exports.APP_DEV = exports.APP_DEV_PREFIX = - exports.DocumentTypes.APP_DEV + exports.SEPARATOR diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts new file mode 100644 index 0000000000..be0e824e61 --- /dev/null +++ b/packages/backend-core/src/db/constants.ts @@ -0,0 +1,58 @@ +export const SEPARATOR = "_" +export const UNICODE_MAX = "\ufff0" + +/** + * Can be used to create a few different forms of querying a view. + */ +export enum AutomationViewModes { + ALL = "all", + AUTOMATION = "automation", + STATUS = "status", +} + +export enum ViewNames { + USER_BY_EMAIL = "by_email", + BY_API_KEY = "by_api_key", + USER_BY_BUILDERS = "by_builders", + LINK = "by_link", + ROUTING = "screen_routes", + AUTOMATION_LOGS = "automation_logs", +} + +export enum DocumentTypes { + USER = "us", + WORKSPACE = "workspace", + CONFIG = "config", + TEMPLATE = "template", + APP = "app", + DEV = "dev", + APP_DEV = "app_dev", + APP_METADATA = "app_metadata", + ROLE = "role", + MIGRATIONS = "migrations", + DEV_INFO = "devinfo", + AUTOMATION_LOG = "log_au", +} + +export const StaticDatabases = { + GLOBAL: { + name: "global-db", + docs: { + apiKeys: "apikeys", + usageQuota: "usage_quota", + licenseInfo: "license_info", + }, + }, + // contains information about tenancy and so on + PLATFORM_INFO: { + name: "global-info", + docs: { + tenants: "tenants", + install: "install", + }, + }, +} + +export const APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR +export const APP_DEV = exports.DocumentTypes.APP_DEV + exports.SEPARATOR +export const APP_DEV_PREFIX = APP_DEV diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index dc7a0454c3..ba3f1dd3e9 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -1,7 +1,7 @@ import { newid } from "../hashing" import { DEFAULT_TENANT_ID, Configs } from "../constants" import env from "../environment" -import { SEPARATOR, DocumentTypes } from "./constants" +import { SEPARATOR, DocumentTypes, UNICODE_MAX, ViewNames } from "./constants" import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy" import fetch from "node-fetch" import { doWithDB, allDbs } from "./index" @@ -12,14 +12,6 @@ import { isDevApp, isDevAppID } from "./conversions" import { APP_PREFIX } from "./constants" import * as events from "../events" -const UNICODE_MAX = "\ufff0" - -export const ViewNames = { - USER_BY_EMAIL: "by_email", - BY_API_KEY: "by_api_key", - USER_BY_BUILDERS: "by_builders", -} - export * from "./constants" export * from "./conversions" export { default as Replication } from "./Replication" @@ -63,6 +55,13 @@ export function getDocParams( } } +/** + * Retrieve the correct index for a view based on default design DB. + */ +export function getQueryIndex(viewName: ViewNames) { + return `database/${viewName}` +} + /** * Generates a new workspace ID. * @returns {string} The new workspace ID which the workspace doc can be stored under. @@ -93,13 +92,17 @@ export function generateGlobalUserID(id?: any) { /** * Gets parameters for retrieving users. */ -export function getGlobalUserParams(globalId: any, otherProps = {}) { +export function getGlobalUserParams(globalId: any, otherProps: any = {}) { if (!globalId) { globalId = "" } + const startkey = otherProps?.startkey return { ...otherProps, - startkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}`, + // need to include this incase pagination + startkey: startkey + ? startkey + : `${DocumentTypes.USER}${SEPARATOR}${globalId}`, endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`, } } @@ -384,7 +387,9 @@ export const getScopedFullConfig = async function ( if (type === Configs.SETTINGS) { if (scopedConfig && scopedConfig.doc) { // overrides affected by environment variables - scopedConfig.doc.config.platformUrl = await getPlatformUrl() + scopedConfig.doc.config.platformUrl = await getPlatformUrl({ + tenantAware: true, + }) scopedConfig.doc.config.analyticsEnabled = await events.analytics.enabled() } else { @@ -393,7 +398,7 @@ export const getScopedFullConfig = async function ( doc: { _id: generateConfigID({ type, user, workspace }), config: { - platformUrl: await getPlatformUrl(), + platformUrl: await getPlatformUrl({ tenantAware: true }), analyticsEnabled: await events.analytics.enabled(), }, }, @@ -434,6 +439,26 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => { return platformUrl } +export function pagination( + data: any[], + pageSize: number, + { paginate, property } = { paginate: true, property: "_id" } +) { + if (!paginate) { + return { data, hasNextPage: false } + } + const hasNextPage = data.length > pageSize + let nextPage = undefined + if (hasNextPage) { + nextPage = property ? data[pageSize]?.[property] : data[pageSize]?._id + } + return { + data: data.slice(0, pageSize), + hasNextPage, + nextPage, + } +} + export async function getScopedConfig(db: any, params: any) { const configDoc = await getScopedFullConfig(db, params) return configDoc && configDoc.config ? configDoc.config : configDoc diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 6eb6b14bc4..ab89eed3b2 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -13,6 +13,7 @@ import deprovisioning from "./context/deprovision" import auth from "./auth" import constants from "./constants" import * as dbConstants from "./db/constants" +import logging from "./logging" // mimic the outer package exports import * as db from "./pkg/db" @@ -49,6 +50,7 @@ const core = { deprovisioning, installation, errors, + logging, ...errorClasses, } diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.js index 9c35336dda..4e6e0b7ba2 100644 --- a/packages/backend-core/src/middleware/authenticated.js +++ b/packages/backend-core/src/middleware/authenticated.js @@ -94,7 +94,6 @@ module.exports = ( user = await getUser(userId, session.tenantId) } user.csrfToken = session.csrfToken - delete user.password authenticated = true } catch (err) { error = err @@ -128,6 +127,8 @@ module.exports = ( } if (!user && tenantId) { user = { tenantId } + } else { + delete user.password } // be explicit if (authenticated !== true) { diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.js index 6c4c0d8883..1721d56a3c 100644 --- a/packages/backend-core/src/middleware/index.js +++ b/packages/backend-core/src/middleware/index.js @@ -2,7 +2,7 @@ const jwt = require("./passport/jwt") const local = require("./passport/local") const google = require("./passport/google") const oidc = require("./passport/oidc") -const { authError } = require("./passport/utils") +const { authError, ssoCallbackUrl } = require("./passport/utils") const authenticated = require("./authenticated") const auditLog = require("./auditLog") const tenancy = require("./tenancy") @@ -20,6 +20,7 @@ module.exports = { tenancy, authError, internalApi, + ssoCallbackUrl, datasource: { google: datasourceGoogle, }, diff --git a/packages/backend-core/src/middleware/passport/google.js b/packages/backend-core/src/middleware/passport/google.js index 858029ca80..7419974cd7 100644 --- a/packages/backend-core/src/middleware/passport/google.js +++ b/packages/backend-core/src/middleware/passport/google.js @@ -1,6 +1,7 @@ const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy - +const { ssoCallbackUrl } = require("./utils") const { authenticateThirdParty } = require("./third-party-common") +const { Configs } = require("../../../constants") const buildVerifyFn = saveUserFn => { return (accessToken, refreshToken, profile, done) => { @@ -57,5 +58,10 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { ) } } + +exports.getCallbackUrl = async function (db, config) { + return ssoCallbackUrl(db, config, Configs.GOOGLE) +} + // expose for testing exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/local.js b/packages/backend-core/src/middleware/passport/local.js index 445893b1df..b955d29102 100644 --- a/packages/backend-core/src/middleware/passport/local.js +++ b/packages/backend-core/src/middleware/passport/local.js @@ -55,6 +55,7 @@ exports.authenticate = async function (ctx, email, password, done) { if (await compare(password, dbUser.password)) { const sessionId = newid() const tenantId = getTenantId() + await createASession(dbUser._id, { sessionId, tenantId }) dbUser.token = jwt.sign( diff --git a/packages/backend-core/src/middleware/passport/oidc.js b/packages/backend-core/src/middleware/passport/oidc.js index 1e93e20b1c..20dbd4669b 100644 --- a/packages/backend-core/src/middleware/passport/oidc.js +++ b/packages/backend-core/src/middleware/passport/oidc.js @@ -1,6 +1,8 @@ const fetch = require("node-fetch") const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy const { authenticateThirdParty } = require("./third-party-common") +const { ssoCallbackUrl } = require("./utils") +const { Configs } = require("../../../constants") const buildVerifyFn = saveUserFn => { /** @@ -89,11 +91,24 @@ function validEmail(value) { * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * @returns Dynamically configured Passport OIDC Strategy */ -exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { +exports.strategyFactory = async function (config, saveUserFn) { try { - const { clientID, clientSecret, configUrl } = config + const verify = buildVerifyFn(saveUserFn) + const strategy = new OIDCStrategy(config, verify) + strategy.name = "oidc" + return strategy + } catch (err) { + console.error(err) + throw new Error("Error constructing OIDC authentication strategy", err) + } +} + +exports.fetchStrategyConfig = async function (enrichedConfig, callbackUrl) { + try { + const { clientID, clientSecret, configUrl } = enrichedConfig if (!clientID || !clientSecret || !callbackUrl || !configUrl) { + //check for remote config and all required elements throw new Error( "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl" ) @@ -109,24 +124,24 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { const body = await response.json() - const verify = buildVerifyFn(saveUserFn) - return new OIDCStrategy( - { - issuer: body.issuer, - authorizationURL: body.authorization_endpoint, - tokenURL: body.token_endpoint, - userInfoURL: body.userinfo_endpoint, - clientID: clientID, - clientSecret: clientSecret, - callbackURL: callbackUrl, - }, - verify - ) + return { + issuer: body.issuer, + authorizationURL: body.authorization_endpoint, + tokenURL: body.token_endpoint, + userInfoURL: body.userinfo_endpoint, + clientID: clientID, + clientSecret: clientSecret, + callbackURL: callbackUrl, + } } catch (err) { console.error(err) - throw new Error("Error constructing OIDC authentication strategy", err) + throw new Error("Error constructing OIDC authentication configuration", err) } } +exports.getCallbackUrl = async function (db, config) { + return ssoCallbackUrl(db, config, Configs.OIDC) +} + // expose for testing exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js index c5e9fe0034..c00ab2ea7d 100644 --- a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js +++ b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js @@ -48,8 +48,8 @@ describe("oidc", () => { it("should create successfully create an oidc strategy", async () => { const oidc = require("../oidc") - - await oidc.strategyFactory(oidcConfig, callbackUrl) + const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl) + await oidc.strategyFactory(enrichedConfig, callbackUrl) expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl) diff --git a/packages/backend-core/src/middleware/passport/utils.js b/packages/backend-core/src/middleware/passport/utils.js index cbb93bfa3b..217130cd6d 100644 --- a/packages/backend-core/src/middleware/passport/utils.js +++ b/packages/backend-core/src/middleware/passport/utils.js @@ -1,3 +1,7 @@ +const { isMultiTenant, getTenantId } = require("../../tenancy") +const { getScopedConfig } = require("../../db/utils") +const { Configs } = require("../../constants") + /** * Utility to handle authentication errors. * @@ -5,6 +9,7 @@ * @param {*} message Message that will be returned in the response body * @param {*} err (Optional) error that will be logged */ + exports.authError = function (done, message, err = null) { return done( err, @@ -12,3 +17,21 @@ exports.authError = function (done, message, err = null) { { message: message } ) } + +exports.ssoCallbackUrl = async (db, config, type) => { + // incase there is a callback URL from before + if (config && config.callbackURL) { + return config.callbackURL + } + const publicConfig = await getScopedConfig(db, { + type: Configs.SETTINGS, + }) + + let callbackUrl = `/api/global/auth` + if (isMultiTenant()) { + callbackUrl += `/${getTenantId()}` + } + callbackUrl += `/${type}/callback` + + return `${publicConfig.platformUrl}${callbackUrl}` +} diff --git a/packages/backend-core/src/objectStore/index.ts b/packages/backend-core/src/objectStore/index.ts index 9bb0760f5b..a7e0b0c134 100644 --- a/packages/backend-core/src/objectStore/index.ts +++ b/packages/backend-core/src/objectStore/index.ts @@ -294,6 +294,16 @@ export const uploadDirectory = async ( await Promise.all(uploads) } +exports.downloadTarballDirect = async (url: string, path: string) => { + path = sanitizeKey(path) + const response = await fetch(url) + if (!response.ok) { + throw new Error(`unexpected response ${response.statusText}`) + } + + await streamPipeline(response.body, zlib.Unzip(), tar.extract(path)) +} + export const downloadTarball = async (url: any, bucketName: any, path: any) => { bucketName = sanitizeBucket(bucketName) path = sanitizeKey(path) diff --git a/packages/backend-core/src/users.js b/packages/backend-core/src/users.js index 4acccda2a0..0c1350a674 100644 --- a/packages/backend-core/src/users.js +++ b/packages/backend-core/src/users.js @@ -1,5 +1,6 @@ const { ViewNames } = require("./db/utils") const { queryGlobalView } = require("./db/views") +const { UNICODE_MAX } = require("./db/constants") /** * Given an email address this will use a view to search through @@ -19,3 +20,24 @@ exports.getGlobalUserByEmail = async email => { return response } + +/** + * Performs a starts with search on the global email view. + */ +exports.searchGlobalUsersByEmail = async (email, opts) => { + if (typeof email !== "string") { + throw new Error("Must provide a string to search by") + } + const lcEmail = email.toLowerCase() + // handle if passing up startkey for pagination + const startkey = opts && opts.startkey ? opts.startkey : lcEmail + let response = await queryGlobalView(ViewNames.USER_BY_EMAIL, { + ...opts, + startkey, + endkey: `${lcEmail}${UNICODE_MAX}`, + }) + if (!response) { + response = [] + } + return Array.isArray(response) ? response : [response] +} diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index e40cddc468..77dbc61425 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -4123,6 +4123,11 @@ passport-oauth1@1.x.x: passport-strategy "1.x.x" utils-merge "1.x.x" +passport-oauth2-refresh@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/passport-oauth2-refresh/-/passport-oauth2-refresh-2.1.0.tgz#c31cd133826383f5539d16ad8ab4f35ca73ce4a4" + integrity sha512-4ML7ooCESCqiTgdDBzNUFTBcPR8zQq9iM6eppEUGMMvLdsjqRL93jKwWm4Az3OJcI+Q2eIVyI8sVRcPFvxcF/A== + passport-oauth2@1.x.x: version "1.6.1" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index d3101946f3..bb80981f5c 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.0.212-alpha.0", + "version": "1.0.219-alpha.0", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "^1.0.212-alpha.0", + "@budibase/string-templates": "^1.0.219-alpha.0", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index b518ac3d92..2d23120046 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -13,6 +13,7 @@ export let size = "M" export let active = false export let fullWidth = false + export let noPadding = false function longPress(element) { if (!longPressable) return @@ -41,6 +42,7 @@ class:spectrum-ActionButton--quiet={quiet} class:spectrum-ActionButton--emphasized={emphasized} class:is-selected={selected} + class:noPadding class:fullWidth class="spectrum-ActionButton spectrum-ActionButton--size{size}" class:active @@ -80,4 +82,8 @@ .active svg { color: var(--spectrum-global-color-blue-600); } + .noPadding { + padding: 0; + min-width: 0; + } diff --git a/packages/bbui/src/Form/Combobox.svelte b/packages/bbui/src/Form/Combobox.svelte index 83927b05db..343af559cb 100644 --- a/packages/bbui/src/Form/Combobox.svelte +++ b/packages/bbui/src/Form/Combobox.svelte @@ -40,5 +40,6 @@ on:change={onChange} on:pick on:type + on:blur /> diff --git a/packages/bbui/src/Form/Core/Combobox.svelte b/packages/bbui/src/Form/Core/Combobox.svelte index 2a4bac4a2c..2835b3cd40 100644 --- a/packages/bbui/src/Form/Core/Combobox.svelte +++ b/packages/bbui/src/Form/Core/Combobox.svelte @@ -52,7 +52,10 @@ {id} type="text" on:focus={() => (focus = true)} - on:blur={() => (focus = false)} + on:blur={() => { + focus = false + dispatch("blur") + }} on:change={onType} value={value || ""} placeholder={placeholder || ""} diff --git a/packages/bbui/src/Notification/Notification.svelte b/packages/bbui/src/Notification/Notification.svelte index 1d21131553..53ab062701 100644 --- a/packages/bbui/src/Notification/Notification.svelte +++ b/packages/bbui/src/Notification/Notification.svelte @@ -1,15 +1,20 @@ -
+
{#if icon} {/if} -
+
{message || ""}
+ {#if action} + +
{actionMessage}
+
+ {/if}
{#if dismissable}
@@ -46,4 +56,15 @@ .spectrum-Toast { pointer-events: all; } + + .wide { + width: 100%; + } + + .actionBody { + justify-content: space-between; + display: flex; + width: 100%; + align-items: center; + } diff --git a/packages/bbui/src/Notification/NotificationDisplay.svelte b/packages/bbui/src/Notification/NotificationDisplay.svelte index eb778f3aa0..0b846f06ce 100644 --- a/packages/bbui/src/Notification/NotificationDisplay.svelte +++ b/packages/bbui/src/Notification/NotificationDisplay.svelte @@ -8,13 +8,15 @@
- {#each $notifications as { type, icon, message, id, dismissable } (id)} + {#each $notifications as { type, icon, message, id, dismissable, action, wide } (id)}
notifications.dismiss(id)} />
diff --git a/packages/bbui/src/Stores/notifications.js b/packages/bbui/src/Stores/notifications.js index 74eed8628a..449d282f24 100644 --- a/packages/bbui/src/Stores/notifications.js +++ b/packages/bbui/src/Stores/notifications.js @@ -20,7 +20,16 @@ export const createNotificationStore = () => { setTimeout(() => (block = false), timeout) } - const send = (message, type = "default", icon = "", autoDismiss = true) => { + const send = ( + message, + { + type = "default", + icon = "", + autoDismiss = true, + action = null, + wide = false, + } + ) => { if (block) { return } @@ -28,7 +37,15 @@ export const createNotificationStore = () => { _notifications.update(state => { return [ ...state, - { id: _id, type, message, icon, dismissable: !autoDismiss }, + { + id: _id, + type, + message, + icon, + dismissable: !autoDismiss, + action, + wide, + }, ] }) if (autoDismiss) { @@ -50,10 +67,11 @@ export const createNotificationStore = () => { return { subscribe, send, - info: msg => send(msg, "info", "Info"), - error: msg => send(msg, "error", "Alert", false), - warning: msg => send(msg, "warning", "Alert"), - success: msg => send(msg, "success", "CheckmarkCircle"), + info: msg => send(msg, { type: "info", icon: "Info" }), + error: msg => + send(msg, { type: "error", icon: "Alert", autoDismiss: false }), + warning: msg => send(msg, { type: "warning", icon: "Alert" }), + success: msg => send(msg, { type: "success", icon: "CheckmarkCircle" }), blockNotifications, dismiss: dismissNotification, } diff --git a/packages/bbui/src/Table/ArrayRenderer.svelte b/packages/bbui/src/Table/ArrayRenderer.svelte index 679973a03a..3755850666 100644 --- a/packages/bbui/src/Table/ArrayRenderer.svelte +++ b/packages/bbui/src/Table/ArrayRenderer.svelte @@ -5,7 +5,7 @@ const displayLimit = 5 - $: badges = value?.slice(0, displayLimit) ?? [] + $: badges = Array.isArray(value) ? value.slice(0, displayLimit) : [] $: leftover = (value?.length ?? 0) - badges.length diff --git a/packages/bbui/src/Table/CellRenderer.svelte b/packages/bbui/src/Table/CellRenderer.svelte index 4dda31240a..246323244a 100644 --- a/packages/bbui/src/Table/CellRenderer.svelte +++ b/packages/bbui/src/Table/CellRenderer.svelte @@ -26,12 +26,20 @@ array: ArrayRenderer, internal: InternalRenderer, } - $: type = schema?.type ?? "string" + $: type = getType(schema) $: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer $: width = schema?.width || "150px" $: cellValue = getCellValue(value, schema.template) + const getType = schema => { + // Use a string renderer for dates if we use a custom template + if (schema?.type === "datetime" && schema?.template) { + return "string" + } + return schema?.type || "string" + } + const getCellValue = (value, template) => { if (!template) { return value diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index baa84c91e0..e01d84e123 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -37,6 +37,7 @@ export let autoSortColumns = true export let compact = false export let customPlaceholder = false + export let placeholderText = "No rows found" const dispatch = createEventDispatcher() @@ -405,7 +406,7 @@ > -
No rows found
+
{placeholderText}
{/if}
diff --git a/packages/builder/cypress.json b/packages/builder/cypress.json index 06bf558946..46f85a52c8 100644 --- a/packages/builder/cypress.json +++ b/packages/builder/cypress.json @@ -13,7 +13,7 @@ "HOST_IP": "" }, "retries": { - "runMode": 2, + "runMode": 1, "openMode": 0 } } diff --git a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js index 38ae881db8..32f62efe1f 100644 --- a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js +++ b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js @@ -16,18 +16,15 @@ filterTests(['all'], () => { it("should add form with multi select picker, containing 5 options", () => { cy.navigateToFrontend() - cy.wait(500) // Add data provider - cy.get(interact.CATEGORY_DATA).click() + cy.get(interact.CATEGORY_DATA, { timeout: 500 }).click() cy.get(interact.COMPONENT_DATA_PROVIDER).click() cy.get(interact.DATASOURCE_PROP_CONTROL).click() cy.get(interact.DROPDOWN).contains("Multi Data").click() - cy.wait(500) // Add Form with schema to match table cy.addComponent("Form", "Form") cy.get(interact.DATASOURCE_PROP_CONTROL).click() cy.get(interact.DROPDOWN).contains("Multi Data").click() - cy.wait(500) // Add multi-select picker to form cy.addComponent("Form", "Multi-select Picker").then(componentId => { cy.get(interact.DATASOURCE_FIELD_CONTROL).type("Test Data").type("{enter}") diff --git a/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js b/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js new file mode 100644 index 0000000000..c615b2b4e6 --- /dev/null +++ b/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js @@ -0,0 +1,131 @@ +import filterTests from "../../support/filterTests" +const interact = require('../../support/interact') + +filterTests(["smoke", "all"], () => { + context("Account Portals", () => { + + const bbUserEmail = "bbuser@test.com" + + before(() => { + cy.login() + cy.deleteApp("Cypress Tests") + cy.createApp("Cypress Tests") + + // Create new user + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 1000}) + cy.createUser(bbUserEmail) + cy.contains("bbuser").click() + cy.wait(500) + + // Reset password + cy.get(".spectrum-ActionButton-label", { timeout: 2000 }).contains("Force password reset").click({ force: true }) + + cy.get(".spectrum-Dialog-grid") + .find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd') + + cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true }) + + // Login as new user and set password + cy.logOut() + cy.get('@pwd').then((pwd) => { + cy.login(bbUserEmail, pwd) + }) + + for (let i = 0; i < 2; i++) { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") + } + cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true }) + cy.logoutNoAppGrid() + }) + + it("should verify Admin Portal", () => { + cy.login() + cy.contains("Users").click() + cy.contains("bbuser").click() + + // Enable Development & Administration access + cy.wait(500) + for (let i = 4; i < 6; i++) { + cy.get(interact.FIELD).eq(i).within(() => { + cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) + cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.enabled') + }) + } + bbUserLogin() + + // Verify available options for Admin portal + cy.get(".spectrum-SideNav") + .should('contain', 'Apps') + //.and('contain', 'Usage') + .and('contain', 'Users') + .and('contain', 'Auth') + .and('contain', 'Email') + .and('contain', 'Organisation') + .and('contain', 'Theming') + .and('contain', 'Update') + //.and('contain', 'Upgrade') + + cy.logOut() + }) + + it("should verify Development Portal", () => { + // Only Development access should be enabled + cy.login() + cy.contains("Users").click() + cy.contains("bbuser").click() + cy.wait(500) + cy.get(interact.FIELD).eq(5).within(() => { + cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) + }) + + bbUserLogin() + + // Verify available options for Admin portal + cy.get(interact.SPECTRUM_SIDENAV) + .should('contain', 'Apps') + //.and('contain', 'Usage') + .and('not.contain', 'Users') + .and('not.contain', 'Auth') + .and('not.contain', 'Email') + .and('not.contain', 'Organisation') + .and('contain', 'Theming') + .and('not.contain', 'Update') + .and('not.contain', 'Upgrade') + + cy.logOut() + }) + + it("should verify Standard Portal", () => { + // Development access should be disabled (Admin access is already disabled) + cy.login() + cy.contains("Users").click() + cy.contains("bbuser").click() + cy.wait(500) + cy.get(interact.FIELD).eq(4).within(() => { + cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) + }) + + bbUserLogin() + + // Verify Standard Portal + cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections + cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button + cy.get(".app").should('not.exist') // No apps -> no roles assigned to user + cy.get(interact.CONTAINER).should('contain', bbUserEmail) // Message containing users email + + cy.logoutNoAppGrid() + }) + + const bbUserLogin = () => { + // Login as bbuser + cy.logOut() + cy.login(bbUserEmail, "test") + } + + after(() => { + cy.login() + // Delete BB user + cy.deleteUser(bbUserEmail) + }) + }) +}) diff --git a/packages/builder/cypress/integration/createUserAndRoles.spec.js b/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js similarity index 50% rename from packages/builder/cypress/integration/createUserAndRoles.spec.js rename to packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js index d47a96ed8d..3c23086136 100644 --- a/packages/builder/cypress/integration/createUserAndRoles.spec.js +++ b/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js @@ -1,28 +1,34 @@ -import filterTests from "../support/filterTests" -const interact = require('../support/interact') +import filterTests from "../../support/filterTests" +const interact = require('../../support/interact') filterTests(["smoke", "all"], () => { - context("Create a User and Assign Roles", () => { + context("User Management", () => { before(() => { cy.login() cy.deleteApp("Cypress Tests") cy.createApp("Cypress Tests") }) - it("should create a user", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) + it("should create a user via basic onboarding", () => { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 1000}) cy.createUser("bbuser@test.com") cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser") }) - it("should confirm there is No Access for a New User", () => { - // Click into the user + it("should confirm basic permission for a New User", () => { + // Basic permission = development & administraton disabled cy.contains("bbuser").click() - cy.wait(500) - // Get No Access table - Confirm it has apps in it - cy.get(interact.SPECTRUM_TABLE).eq(1).should("not.contain", "No rows found") - // Get Configure Roles table - Confirm it has no apps + // Confirm development and admin access are disabled + for (let i = 4; i < 6; i++) { + cy.wait(500) + cy.get(interact.FIELD).eq(i).within(() => { + //cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.disabled') + cy.get(".spectrum-Switch-switch").should('not.be.checked') + }) + } + // Existing apps appear within the No Access table + cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).eq(1).should("not.contain", "No rows found") + // Configure roles table should not contain apps cy.get(interact.SPECTRUM_TABLE).eq(0).contains("No rows found") }) @@ -40,20 +46,16 @@ filterTests(["smoke", "all"], () => { cy.createApp(name) } else { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) - cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) + cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true }) cy.createAppFromScratch(name) } } } }) // Navigate back to the user - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 500}) cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click() - cy.wait(500) - cy.get(interact.SPECTRUM_TABLE).contains("bbuser").click() - cy.wait(1000) + cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).contains("bbuser").click() for (let i = 0; i < 3; i++) { cy.get(interact.SPECTRUM_TABLE, { timeout: 3000}) .eq(1) @@ -62,31 +64,28 @@ filterTests(["smoke", "all"], () => { .find(interact.SPECTRUM_TABLE_CELL) .eq(0) .click() - cy.wait(500) - cy.get(interact.SPECTRUM_DIALOG_GRID) + cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 500 }) .contains("Choose an option") .click() .then(() => { - cy.wait(1000) if (i == 0) { - cy.get(interact.SPECTRUM_MENU).contains("Admin").click({ force: true }) + cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Admin").click({ force: true }) } else if (i == 1) { - cy.get(interact.SPECTRUM_MENU).contains("Power").click({ force: true }) + cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Power").click({ force: true }) } else if (i == 2) { - cy.get(interact.SPECTRUM_MENU).contains("Basic").click({ force: true }) + cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Basic").click({ force: true }) } - cy.wait(1000) - cy.get(interact.SPECTRUM_BUTTON) + cy.get(interact.SPECTRUM_BUTTON, { timeout: 1000 }) .contains("Update role") .click({ force: true }) }) cy.reload() + cy.wait(1000) } // Confirm roles exist within Configure roles table - cy.wait(2000) - cy.get(interact.SPECTRUM_TABLE) + cy.get(interact.SPECTRUM_TABLE, { timeout: 2000 }) .eq(0) .within(assginedRoles => { expect(assginedRoles).to.contain("Admin") @@ -112,21 +111,19 @@ filterTests(["smoke", "all"], () => { .click() .then(() => { cy.get(interact.SPECTRUM_PICKER).eq(1).click({ force: true }) - cy.wait(500) - cy.get(interact.SPECTRUM_POPOVER).contains("No Access").click() + cy.get(interact.SPECTRUM_POPOVER, { timeout: 500 }).contains("No Access").click() }) cy.get(interact.SPECTRUM_BUTTON) .contains("Update role") .click({ force: true }) - cy.wait(1000) } }) // Confirm Configure roles table no longer has any apps in it - cy.get(interact.SPECTRUM_TABLE).eq(0).contains("No rows found") + cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).eq(0).contains("No rows found") }) } - it("should enable Developer access", () => { + it("should enable Developer access and verify application access", () => { // Enable Developer access cy.get(interact.FIELD) .eq(4) @@ -158,15 +155,15 @@ filterTests(["smoke", "all"], () => { }) }) - it("should disable Developer access", () => { + it("should disable Developer access and verify application access", () => { // Disable Developer access - cy.get(".field") + cy.get(interact.FIELD) .eq(4) .within(() => { cy.get(".spectrum-Switch-input").click({ force: true }) }) // Configure roles table should now be empty - cy.get(".container") + cy.get(interact.CONTAINER) .contains("Configure roles") .parent() .within(() => { @@ -174,22 +171,64 @@ filterTests(["smoke", "all"], () => { }) }) + it("Should edit user details within user details page", () => { + // Add First name + cy.get(interact.FIELD, { timeout: 500 }).eq(2).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 500 }).type("bb") + }) + // Add Last name + cy.get(interact.FIELD).eq(3).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).type("test") + }) + cy.get(interact.FIELD).eq(0).click() + // Reload page + cy.reload() + + // Confirm details have been saved + cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb") + }) + cy.get(interact.FIELD).eq(3).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 500 }).should('have.value', "test") + }) + }) + + it("should reset the users password", () => { + cy.get(interact.REGENERATE, { timeout: 500 }).contains("Force password reset").click({ force: true }) + + // Reset password modal + cy.get(interact.SPECTRUM_DIALOG_GRID) + .find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd') + cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true }) + + // Logout, then login with new password + cy.logOut() + cy.get('@pwd').then((pwd) => { + cy.login("bbuser@test.com", pwd) + }) + + // Reset password screen + for (let i = 0; i < 2; i++) { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") + } + cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true }) + + // Confirm user logged in afer password change + cy.get(".avatar > .icon").click({ force: true }) + + cy.get(".spectrum-Menu-item").contains("Update user information").click({ force: true }) + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) + .eq(0) + .invoke('val').should('eq', 'bbuser@test.com') + + // Logout and login as previous user + cy.logoutNoAppGrid() + cy.login() + }) + it("should delete a user", () => { - // Click Delete user button - cy.get(interact.SPECTRUM_BUTTON) - .contains("Delete user") - .click({ force: true }) - .then(() => { - // Confirm deletion within modal - cy.wait(500) - cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { - cy.get(interact.SPECTRUM_BUTTON) - .contains("Delete user") - .click({ force: true }) - cy.wait(4000) - }) - }) - cy.get(interact.SPECTRUM_TABLE).should("not.have.text", "bbuser") + cy.deleteUser("bbuser@test.com") + cy.get(interact.SPECTRUM_TABLE, { timeout: 4000 }).should("not.have.text", "bbuser") }) }) }) diff --git a/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js b/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js new file mode 100644 index 0000000000..7827275620 --- /dev/null +++ b/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js @@ -0,0 +1,108 @@ +import filterTests from "../../support/filterTests" +const interact = require('../../support/interact') + +filterTests(["smoke", "all"], () => { + context("User Settings Menu", () => { + + before(() => { + cy.login() + }) + + it("should update user information via user settings menu", () => { + const fname = "test" + const lname = "user" + + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.updateUserInformation(fname, lname) + + // Go to user info and confirm name update + cy.contains("Users").click() + cy.contains("test@test.com").click() + + cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', fname) + }) + cy.get(interact.FIELD).eq(3).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', lname) + }) + }) + + it("should allow copying of the users API key", () => { + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("View API key").click({ force: true }) + cy.get(interact.SPECTRUM_DIALOG_CONTENT).within(() => { + cy.get(interact.SPECTRUM_ICON).click({force: true}) + }) + // There may be timing issues with this on the smoke build + cy.wait(500) + cy.get(".spectrum-Toast-content") + .contains("URL copied to clipboard") + .should("be.visible") + }) + + it("should allow API key regeneration", () => { + // Get initial API key value + cy.get(interact.SPECTRUM_DIALOG_CONTENT) + .find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('keyOne') + + // Click re-generate key button + cy.get("button").contains("Re-generate key").click({ force: true }) + + // Verify API key was changed + cy.get(interact.SPECTRUM_DIALOG_CONTENT).within(() => { + cy.get('@keyOne').then((keyOne) => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').should('not.eq', keyOne) + }) + }) + cy.closeModal() + }) + + it("should update password", () => { + // Access Update password modal + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Update password").click({ force: true }) + + // Enter new password and update + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { + for (let i = 0; i < 2; i++) { + // password set to 'newpwd' + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("newpwd") + } + cy.get("button").contains("Update password").click({ force: true }) + }) + + // Logout & in with new password + cy.logOut() + cy.login("test@test.com", "newpwd") + }) + + it("should open and close developer mode", () => { + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + + // Close developer mode & verify + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Close developer mode").click({ force: true }) + cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections + cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button + cy.get(".app").should('not.exist') // At least one app should be available + + // Open developer mode & verify + cy.get(".avatar > .icon").click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Open developer mode").click({ force: true }) + cy.get(interact.SPECTRUM_SIDENAV).should('exist') // config sections available + cy.get(interact.CREATE_APP_BUTTON).should('exist') // create app button available + cy.get(interact.APP_TABLE).should('exist') // App table available + }) + + after(() => { + // Change password back to original value + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Update password").click({ force: true }) + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { + for (let i = 0; i < 2; i++) { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") + } + cy.get("button").contains("Update password").click({ force: true }) + }) + }) + }) +}) diff --git a/packages/builder/cypress/integration/appOverview.spec.js b/packages/builder/cypress/integration/appOverview.spec.js index 4b9d072338..d718f95b9f 100644 --- a/packages/builder/cypress/integration/appOverview.spec.js +++ b/packages/builder/cypress/integration/appOverview.spec.js @@ -136,7 +136,6 @@ filterTests(["all"], () => { .within(() => { cy.get(".confirm-wrap button").click({ force: true }) }) - cy.wait(1000) cy.visit(`${Cypress.config().baseUrl}/builder`) cy.get(".appTable .app-row-actions button") @@ -158,12 +157,9 @@ filterTests(["all"], () => { .contains("Manage") .eq(0) .click({ force: true }) - cy.wait(1000) - cy.get(".app-overview-actions-icon").within(() => { - cy.get(".spectrum-Icon").click({ force: true }) - }) - cy.get(".spectrum-Menu").contains("Edit icon").click() + cy.get(".edit-hover", { timeout: 1000 }).eq(0).click({ force: true }) // Select random icon + cy.wait(400) cy.get(".grid").within(() => { cy.get(".icon-item") .eq(Math.floor(Math.random() * 23) + 1) @@ -182,6 +178,7 @@ filterTests(["all"], () => { cy.get("@iconChange").its("response.statusCode").should("eq", 200) // Confirm icon has changed from default // Confirm colour has been applied + cy.get(".spectrum-ActionButton-label").contains("Back").click({ force: true }) cy.get(".appTable", { timeout: 2000 }).within(() => { cy.get("[aria-label]") .eq(0) @@ -265,10 +262,9 @@ filterTests(["all"], () => { //Downgrade the app for the test cy.alterAppVersion(appId, "0.0.1-alpha.0").then(() => { cy.reload() - cy.wait(1000) cy.log("Current deployment version: " + clientPackage.version) - cy.get(".version-status a").contains("Update").click() + cy.get(".version-status a", { timeout: 1000 }).contains("Update").click() cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") cy.get(".version-section .page-action button") @@ -336,8 +332,7 @@ filterTests(["all"], () => { }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.get(".appTable .app-row-actions button") + cy.get(".appTable .app-row-actions button", { timeout: 1000 }) .contains("Manage") .eq(0) .click({ force: true }) @@ -345,8 +340,7 @@ filterTests(["all"], () => { cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") cy.get(".details-section .page-action .spectrum-Button").scrollIntoView() - cy.wait(1000) - cy.get(".details-section .page-action .spectrum-Button").should( + cy.get(".details-section .page-action .spectrum-Button", { timeout: 1000 }).should( "be.disabled" ) }) @@ -375,27 +369,21 @@ filterTests(["all"], () => { .contains("Copy App ID") .click({ force: true }) }) - + cy.get(".spectrum-Toast-content") - .contains("App ID copied to clipboard.") - .should("be.visible") + .contains("App ID copied to clipboard.") + .should("be.visible") }) - it("Should allow unpublishing of the application", () => { + it("Should allow unpublishing of the application via the Unpublish link", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) cy.get(".appTable .app-row-actions button") .contains("Manage") .eq(0) .click({ force: true }) - cy.get(".app-overview-actions-icon > .icon").click({ force: true }) - cy.get("[data-cy='app-overview-menu-popover']") - .eq(0) - .within(() => { - cy.get(".spectrum-Menu-item") - .contains("Unpublish") - .click({ force: true }) - cy.wait(500) + cy.get(`[data-cy="app-status"]`).within(() => { + cy.contains("Unpublish").click({ force: true }) }) cy.get("[data-cy='unpublish-modal']") @@ -406,9 +394,8 @@ filterTests(["all"], () => { cy.get(".overview-tab [data-cy='app-status']").within(() => { cy.get(".status-display").contains("Unpublished") - cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should( - "exist" - ) + cy.get(".status-display .icon svg[aria-label='GlobeStrike']") + .should("exist") }) }) diff --git a/packages/builder/cypress/integration/appPublishWorkflow.spec.js b/packages/builder/cypress/integration/appPublishWorkflow.spec.js index f0a7dd791a..e65c01c1b6 100644 --- a/packages/builder/cypress/integration/appPublishWorkflow.spec.js +++ b/packages/builder/cypress/integration/appPublishWorkflow.spec.js @@ -11,9 +11,8 @@ filterTests(['all'], () => { it("Should reflect the unpublished status correctly", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.get(interact.APP_TABLE_STATUS).eq(0) + cy.get(interact.APP_TABLE_STATUS, { timeout: 3000 }).eq(0) .within(() => { cy.contains("Unpublished") cy.get(interact.GLOBESTRIKE).should("exist") @@ -35,11 +34,10 @@ filterTests(['all'], () => { cy.get(interact.DEPLOY_APP_MODAL).should("be.visible") .within(() => { cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force : true }) - cy.wait(1000) }); //Verify that the app url is presented correctly to the user - cy.get(interact.DEPLOY_SUCCESS_MODAL) + cy.get(interact.DEPLOY_SUCCESS_MODAL, { timeout: 1000 }) .should("be.visible") .within(() => { let appUrl = Cypress.config().baseUrl + '/app/cypress-tests' @@ -48,9 +46,8 @@ filterTests(['all'], () => { }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.get(interact.APP_TABLE_STATUS).eq(0) + cy.get(interact.APP_TABLE_STATUS, { timeout: 3000 }).eq(0) .within(() => { cy.contains("Published") cy.get(interact.GLOBE).should("exist") @@ -58,7 +55,7 @@ filterTests(['all'], () => { cy.get(interact.APP_TABLE_ROW_ACTION).eq(0) .within(() => { - cy.get(interact.SPECTRUM_BUTTON).contains("View") + cy.get(interact.SPECTRUM_BUTTON).contains("Manage") cy.get(interact.SPECTRUM_BUTTON).contains("Edit").click({ force: true }) }) @@ -83,24 +80,25 @@ filterTests(['all'], () => { cy.get("svg[aria-label='Globe']").should("exist") }) - cy.get(interact.APP_TABLE_ROW_ACTION).eq(0) + cy.get(interact.APP_TABLE).eq(0) .within(() => { - cy.get(interact.SPECTRUM_BUTTON).contains("View") cy.get(interact.APP_TABLE_APP_NAME).click({ force: true }) }) - cy.get(interact.SPECTRUM_LINK).contains('Unpublish').click(); + cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true }) + + cy.get("[data-cy='publish-popover-menu']") + .within(() => { + cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true }) + }) cy.get(interact.UNPUBLISH_MODAL).should("be.visible") .within(() => { cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true } )}) - cy.get(interact.DEPLOYMENT_TOP_NAV_GLOBESTRIKE).should("exist") - cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(interact.APP_TABLE_STATUS).eq(0).contains("Unpublished") + cy.get(interact.APP_TABLE_STATUS, { timeout: 1000 }).eq(0).contains("Unpublished") }) }) diff --git a/packages/builder/cypress/integration/autoScreensUI.spec.js b/packages/builder/cypress/integration/autoScreensUI.spec.js index eebeac3e71..ca997479ae 100644 --- a/packages/builder/cypress/integration/autoScreensUI.spec.js +++ b/packages/builder/cypress/integration/autoScreensUI.spec.js @@ -5,6 +5,7 @@ filterTests(['smoke', 'all'], () => { context("Auto Screens UI", () => { before(() => { cy.login() + cy.deleteAllApps() }) it("should disable the autogenerated screen options if no sources are available", () => { diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js index df617e3d9f..00c875e4fa 100644 --- a/packages/builder/cypress/integration/createApp.spec.js +++ b/packages/builder/cypress/integration/createApp.spec.js @@ -6,14 +6,14 @@ filterTests(['smoke', 'all'], () => { before(() => { cy.login() - cy.deleteApp("Cypress Tests") + cy.deleteAllApps() }) if (!(Cypress.env("TEST_ENV"))) { it("should show the new user UI/UX", () => { - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`) //added /portal/apps/create + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`, { timeout: 5000 }) //added /portal/apps/create + cy.wait(1000) cy.get(interact.CREATE_APP_BUTTON).contains('Start from scratch').should("exist") - cy.get(interact.CREATE_APP_BUTTON).should("exist") cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist") cy.get(interact.TEMPLATE_CATEGORY).should("exist") @@ -23,7 +23,7 @@ filterTests(['smoke', 'all'], () => { } it("should provide filterable templates", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.wait(500) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) @@ -48,18 +48,10 @@ filterTests(['smoke', 'all'], () => { }) it("should enforce a valid url before submission", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) // Start create app process. If apps already exist, click second button - cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) - } - }) + cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true }) const appName = "Cypress Tests" cy.get(interact.SPECTRUM_MODAL).within(() => { @@ -92,21 +84,16 @@ filterTests(['smoke', 'all'], () => { it("should create the first application from scratch", () => { const appName = "Cypress Tests" - cy.createApp(appName) + cy.createApp(appName, false) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) cy.applicationInAppTable(appName) cy.deleteApp(appName) }) it("should create the first application from scratch with a default name", () => { - cy.createApp() - - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - + cy.createApp("", false) cy.applicationInAppTable("My app") cy.deleteApp("My app") }) @@ -116,26 +103,22 @@ filterTests(['smoke', 'all'], () => { cy.updateUserInformation("Ted", "Userman") - cy.createApp() + cy.createApp("", false) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) cy.applicationInAppTable("Teds app") cy.deleteApp("Teds app") - cy.wait(2000) //Accomodate names that end in 'S' cy.updateUserInformation("Chris", "Userman") - cy.createApp() + cy.createApp("", false) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) cy.applicationInAppTable("Chris app") cy.deleteApp("Chris app") - cy.wait(2000) cy.updateUserInformation("", "") }) @@ -145,7 +128,7 @@ filterTests(['smoke', 'all'], () => { cy.importApp(exportedApp, "") - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 2000 }) cy.applicationInAppTable("My app") @@ -224,14 +207,12 @@ filterTests(['smoke', 'all'], () => { cy.createApp(appName) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) // Create second app const secondAppName = "Second App Demo" cy.createApp(secondAppName) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) //Both applications should exist and be searchable cy.searchForApplication(appName) diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js index 6a4b70f8dc..b5ff406297 100644 --- a/packages/builder/cypress/integration/createAutomation.spec.js +++ b/packages/builder/cypress/integration/createAutomation.spec.js @@ -16,17 +16,15 @@ filterTests(['smoke', 'all'], () => { cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.get("input").type("Add Row") cy.contains("Row Created").click({ force: true }) - cy.wait(500) - cy.get(interact.SPECTRUM_BUTTON_CTA).click() + cy.get(interact.SPECTRUM_BUTTON_CTA, { timeout: 500 }).click() }) // Setup trigger cy.get(interact.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('[aria-label="AddCircle"]', { timeout: 2000 }).eq(1).click() cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.wait(1000) cy.contains("Create Row").trigger('mouseover').click().click() @@ -43,11 +41,9 @@ filterTests(['smoke', 'all'], () => { cy.contains("Finish and test automation").click() cy.get(interact.MODAL_INNER_WRAPPER).within(() => { - cy.wait(1000) - cy.get(interact.SPECTRUM_PICKER_LABEL).click() + cy.get(interact.SPECTRUM_PICKER_LABEL, { timeout: 1000 }).click() cy.contains("dog").click() - cy.wait(1000) - cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }) .first() .type("automationGoodboy") cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) diff --git a/packages/builder/cypress/integration/createScreen.js b/packages/builder/cypress/integration/createScreen.spec.js similarity index 100% rename from packages/builder/cypress/integration/createScreen.js rename to packages/builder/cypress/integration/createScreen.spec.js diff --git a/packages/builder/cypress/integration/createTable.spec.js b/packages/builder/cypress/integration/createTable.spec.js index 7d55a1f03c..da73c19fa6 100644 --- a/packages/builder/cypress/integration/createTable.spec.js +++ b/packages/builder/cypress/integration/createTable.spec.js @@ -10,9 +10,8 @@ filterTests(["smoke", "all"], () => { it("should create a new Table", () => { cy.createTable("dog") - cy.wait(1000) // Check if Table exists - cy.get(interact.TABLE_TITLE_H1).should("have.text", "dog") + cy.get(interact.TABLE_TITLE_H1, { timeout: 1000 }).should("have.text", "dog") }) it("adds a new column to the table", () => { @@ -40,7 +39,7 @@ filterTests(["smoke", "all"], () => { it("edits a row", () => { cy.contains("button", "Edit").click({ force: true }) - cy.wait(1000) + cy.wait(500) cy.get(interact.SPECTRUM_MODAL_INPUT).clear() cy.get(interact.SPECTRUM_MODAL_INPUT).type("Updated") cy.contains("Save").click() @@ -63,8 +62,7 @@ filterTests(["smoke", "all"], () => { cy.addRow([i]) } cy.reload() - cy.wait(2000) - cy.get(interact.SPECTRUM_PAGINATION).within(() => { + cy.get(interact.SPECTRUM_PAGINATION, { timeout: 2000 }).within(() => { cy.get(interact.SPECTRUM_ACTION_BUTTON).eq(1).click() }) cy.get(interact.SPECTRUM_PAGINATION).within(() => { @@ -79,10 +77,9 @@ filterTests(["smoke", "all"], () => { cy.get(interact.SPECTRUM_BUTTON).click({ force: true }) }) cy.get(interact.SPECTRUM_DIALOG_GRID).contains("Delete").click({ force: true }) - cy.wait(1000) // Confirm table only has one page - cy.get(interact.SPECTRUM_PAGINATION).within(() => { + cy.get(interact.SPECTRUM_PAGINATION, { timeout: 1000 }).within(() => { cy.get(interact.SPECTRUM_ACTION_BUTTON).eq(1).should("not.be.enabled") }) }) diff --git a/packages/builder/cypress/integration/createView.spec.js b/packages/builder/cypress/integration/createView.spec.js index e554f6f866..a2d09d97bf 100644 --- a/packages/builder/cypress/integration/createView.spec.js +++ b/packages/builder/cypress/integration/createView.spec.js @@ -69,8 +69,8 @@ filterTests(['smoke', 'all'], () => { cy.get(interact.SPECTRUM_BUTTON).contains("Save").click({ force: true }) }) - cy.wait(1000) + cy.wait(1000) cy.get(interact.TITLE).then($headers => { expect($headers).to.have.length(7) const headers = Array.from($headers).map(header => diff --git a/packages/builder/cypress/integration/customThemingProperties.spec.js b/packages/builder/cypress/integration/customThemingProperties.spec.js index ed3478ca67..e9de0985d0 100644 --- a/packages/builder/cypress/integration/customThemingProperties.spec.js +++ b/packages/builder/cypress/integration/customThemingProperties.spec.js @@ -34,7 +34,6 @@ filterTests(['all'], () => { Large = 16px */ it("should test button roundness", () => { const buttonRoundnessValues = ["0", "4px", "8px", "16px"] - cy.wait(1000) // Add button, change roundness and confirm value cy.addComponent("Button", null).then((componentId) => { buttonRoundnessValues.forEach(function (item, index){ diff --git a/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js b/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js index 1bee7b5ec1..14653d8286 100644 --- a/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js +++ b/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js @@ -17,11 +17,10 @@ filterTests(['all'], () => { // 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(".item-list", { timeout: 1000 }).contains(datasource).click() cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Button").contains("Continue").click({ force: true }) }) diff --git a/packages/builder/cypress/integration/datasources/mySql.spec.js b/packages/builder/cypress/integration/datasources/mySql.spec.js index 98bb2f2acf..b79f5af9c6 100644 --- a/packages/builder/cypress/integration/datasources/mySql.spec.js +++ b/packages/builder/cypress/integration/datasources/mySql.spec.js @@ -111,10 +111,9 @@ filterTests(["all"], () => { // 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") + cy.get(".spectrum-Table", { timeout: 1000 }) .eq(1) .find(".spectrum-Table-row") .its("length") @@ -136,15 +135,15 @@ filterTests(["all"], () => { cy.get(".spectrum-Table") .eq(1) .within(() => { - cy.get(".spectrum-Table-row").eq(0).click({ force: true }) - cy.wait(500) + cy.get(".spectrum-Table-cell").eq(0).click({ force: true }) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Button") .contains("Delete") .click({ force: true }) }) cy.reload() + cy.wait(500) } // Confirm relationships no longer exist cy.get(".spectrum-Body").should( @@ -217,9 +216,8 @@ filterTests(["all"], () => { 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", { timeout: 1000 }).should("not.contain", queryName) }) } }) diff --git a/packages/builder/cypress/integration/datasources/oracle.spec.js b/packages/builder/cypress/integration/datasources/oracle.spec.js index 4c4d33d654..92a5737ff9 100644 --- a/packages/builder/cypress/integration/datasources/oracle.spec.js +++ b/packages/builder/cypress/integration/datasources/oracle.spec.js @@ -20,7 +20,7 @@ filterTests(["all"], () => { .click({ force: true }) cy.wait(500) // Confirm config contains localhost - cy.get(".spectrum-Textfield-input") + cy.get(".spectrum-Textfield-input", { timeout: 500 }) .eq(1) .should("have.value", "localhost") // Add another Oracle data source, configure & skip table fetch @@ -140,9 +140,8 @@ filterTests(["all"], () => { .eq(1) .within(() => { cy.get(".spectrum-Table-row").eq(0).click() - cy.wait(500) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Button") .contains("Delete") .click({ force: true }) @@ -221,10 +220,9 @@ filterTests(["all"], () => { 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", { timeout: 1000 }).should("not.contain", queryName) }) } }) diff --git a/packages/builder/cypress/integration/datasources/postgreSql.spec.js b/packages/builder/cypress/integration/datasources/postgreSql.spec.js index 7448e6b27d..de959e203c 100644 --- a/packages/builder/cypress/integration/datasources/postgreSql.spec.js +++ b/packages/builder/cypress/integration/datasources/postgreSql.spec.js @@ -35,6 +35,7 @@ filterTests(["all"], () => { // Check response from datasource after adding configuration cy.wait("@datasource") cy.get("@datasource").its("response.statusCode").should("eq", 200) + cy.wait(2000) // Confirm fetch tables was successful cy.get(".spectrum-Table") .eq(0) @@ -113,13 +114,13 @@ filterTests(["all"], () => { cy.get(".spectrum-Table") .eq(1) .within(() => { - cy.get(".spectrum-Table-row").eq(0).click({ force: true }) - cy.wait(500) + cy.get(".spectrum-Table-cell").eq(0).click({ force: true }) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Button").contains("Delete").click({ force: true }) }) cy.reload() + cy.wait(500) // Confirm relationship was deleted cy.get(".spectrum-Table") .eq(1) @@ -159,7 +160,7 @@ filterTests(["all"], () => { switchSchema("randomText") // No tables displayed - cy.get(".spectrum-Body").eq(2).should("contain", "No tables found") + cy.get(".spectrum-Body", { timeout: 5000 }).eq(2).should("contain", "No tables found") // Previously created query should be visible cy.get(".spectrum-Table").should("contain", queryName) @@ -170,7 +171,7 @@ filterTests(["all"], () => { switchSchema("1") // Confirm tables exist - Check for specific one - cy.get(".spectrum-Table").eq(0).should("contain", "test") + cy.get(".spectrum-Table", { timeout: 5000 }).eq(0).should("contain", "test") cy.get(".spectrum-Table") .eq(0) .find(".spectrum-Table-row") @@ -184,7 +185,7 @@ filterTests(["all"], () => { switchSchema("public") // Confirm tables exist - again - cy.get(".spectrum-Table").eq(0).should("contain", "REGIONS") + cy.get(".spectrum-Table", { timeout: 5000 }).eq(0).should("contain", "REGIONS") cy.get(".spectrum-Table") .eq(0) .find(".spectrum-Table-row") @@ -230,7 +231,9 @@ filterTests(["all"], () => { // 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(".spectrum-Button", { timeout: 500 }).contains("Save Query").click({ force: true }) + //cy.reload() + //cy.wait(500) cy.get(".nav-item").should("contain", queryRename) }) @@ -247,9 +250,8 @@ filterTests(["all"], () => { 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", { timeout: 1000 }).should("not.contain", queryName) }) const switchSchema = schema => { @@ -271,7 +273,7 @@ filterTests(["all"], () => { .click({ force: true }) }) cy.reload() - cy.wait(5000) + cy.wait(1000) } } }) diff --git a/packages/builder/cypress/integration/datasources/rest.spec.js b/packages/builder/cypress/integration/datasources/rest.spec.js index a15487c01b..488c30c0cf 100644 --- a/packages/builder/cypress/integration/datasources/rest.spec.js +++ b/packages/builder/cypress/integration/datasources/rest.spec.js @@ -14,8 +14,7 @@ filterTests(["smoke", "all"], () => { // 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.get(".spectrum-Button", { timeout: 500 }).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 }) @@ -36,8 +35,7 @@ filterTests(["smoke", "all"], () => { // 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(".stats", { timeout: 1000 }).within(() => { cy.get(".spectrum-FieldLabel") .eq(0) .should("contain", 200) diff --git a/packages/builder/cypress/integration/renameAnApplication.spec.js b/packages/builder/cypress/integration/renameAnApplication.spec.js index 703535a507..370efadff2 100644 --- a/packages/builder/cypress/integration/renameAnApplication.spec.js +++ b/packages/builder/cypress/integration/renameAnApplication.spec.js @@ -13,16 +13,13 @@ filterTests(["all"], () => { const appRename = "Cypress Renamed" // Rename app, Search for app, Confirm name was changed cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) renameApp(appName, appRename) cy.reload() - cy.wait(1000) cy.searchForApplication(appRename) cy.get(interact.APP_TABLE).find(interact.TITLE).should("have.length", 1) cy.applicationInAppTable(appRename) // Set app name back to Cypress Tests cy.reload() - cy.wait(1000) renameApp(appRename, appName) }) @@ -43,7 +40,6 @@ filterTests(["all"], () => { }) // Rename app, Search for app, Confirm name was changed cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) renameApp(appName, appRename, true) cy.get(interact.APP_TABLE).find(interact.WRAPPER).should("have.length", 1) cy.applicationInAppTable(appRename) @@ -52,13 +48,9 @@ filterTests(["all"], () => { it("Should try to rename an application to have no name", () => { const appName = "Cypress Tests" cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) renameApp(appName, " ", false, true) - cy.wait(500) // Close modal and confirm name has not been changed - cy.get(interact.SPECTRUM_DIALOG_GRID).contains("Cancel").click() - cy.reload() - cy.wait(1000) + cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 }).contains("Cancel").click() cy.applicationInAppTable(appName) }) @@ -66,8 +58,7 @@ filterTests(["all"], () => { // 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(interact.SPECTRUM_BUTTON) + cy.get(interact.SPECTRUM_BUTTON), { timeout: 500 } .contains("Create app") .click({ force: true }) cy.contains(/Start from scratch/).click() @@ -90,13 +81,10 @@ filterTests(["all"], () => { const numberName = 12345 const specialCharName = "£$%^" cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) renameApp(appName, numberName) cy.reload() - cy.wait(1000) cy.applicationInAppTable(numberName) cy.reload() - cy.wait(1000) renameApp(numberName, specialCharName) cy.get(interact.ERROR).should( "have.text", @@ -104,13 +92,12 @@ filterTests(["all"], () => { ) // 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.get(interact.APP_TABLE).within(() => { + cy.get(interact.APP_TABLE, { timeout: 1000 }).within(() => { cy.get(".app-row-actions button") .contains("Manage") .eq(0) diff --git a/packages/builder/cypress/integration/revertApp.spec.js b/packages/builder/cypress/integration/revertApp.spec.js index 93501ab972..4c6f245b76 100644 --- a/packages/builder/cypress/integration/revertApp.spec.js +++ b/packages/builder/cypress/integration/revertApp.spec.js @@ -36,8 +36,8 @@ filterTests(['smoke', 'all'], () => { cy.get(interact.SPECTRUM_BUTTON_GROUP).within(() => { cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force: true }) }) - cy.wait(1000) - cy.get(interact.SPECTRUM_BUTTON_GROUP).within(() => { + cy.wait(1000) // Wait for next modal to finish loading + cy.get(interact.SPECTRUM_BUTTON_GROUP, { timeout: 1000 }).within(() => { cy.get(interact.SPECTRUM_BUTTON).contains("Done").click({ force: true }) }) @@ -50,18 +50,17 @@ filterTests(['smoke', 'all'], () => { cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { // Click Revert cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true }) - cy.wait(1000) + cy.wait(2000) // Wait for app to finish reverting }) // Confirm Paragraph component is still visible - cy.get(interact.ROOT).contains("New Paragraph") + cy.get(interact.ROOT, { timeout: 1000 }).contains("New Paragraph") // Confirm Button component is not visible - cy.get(interact.ROOT).should("not.have.text", "New Button") - cy.wait(500) + cy.get(interact.ROOT, { timeout: 1000 }).should("not.have.text", "New Button") }) it("should enter incorrect app name when reverting", () => { // Click Revert - cy.get(interact.TOP_RIGHT_NAV).within(() => { + cy.get(interact.TOP_RIGHT_NAV, { timeout: 1000 }).within(() => { cy.get(interact.AREA_LABEL_REVERT).click({ force: true }) }) // Enter incorrect app name diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index 29aabc4611..9d77b89c57 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -3,7 +3,7 @@ Cypress.on("uncaught:exception", () => { }) // ACCOUNTS & USERS -Cypress.Commands.add("login", () => { +Cypress.Commands.add("login", (email, password) => { cy.visit(`${Cypress.config().baseUrl}/builder`) cy.wait(2000) cy.url().then(url => { @@ -17,8 +17,13 @@ Cypress.Commands.add("login", () => { if (url.includes("builder/auth/login") || url.includes("builder/admin")) { // login cy.contains("Sign in to Budibase").then(() => { - cy.get("input").first().type("test@test.com") - cy.get('input[type="password"]').type("test") + if (email == null) { + cy.get("input").first().type("test@test.com") + cy.get('input[type="password"]').type("test") + } else { + cy.get("input").first().type(email) + cy.get('input[type="password"]').type(password) + } cy.get("button").first().click({ force: true }) cy.wait(1000) }) @@ -27,7 +32,7 @@ Cypress.Commands.add("login", () => { }) Cypress.Commands.add("logOut", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 2000 }) cy.get(".user-dropdown .avatar > .icon").click({ force: true }) cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { cy.get("li[data-cy='user-logout']").click({ force: true }) @@ -35,24 +40,58 @@ Cypress.Commands.add("logOut", () => { cy.wait(2000) }) +Cypress.Commands.add("logoutNoAppGrid", () => { + // Logs user out when app grid is not present + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.get(".avatar > .icon").click({ force: true }) + cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { + cy.get(".spectrum-Menu-item").contains("Log out").click({ force: true }) + }) + cy.wait(2000) +}) + Cypress.Commands.add("createUser", email => { // quick hacky recorded way to create a user cy.contains("Users").click() cy.get(`[data-cy="add-user"]`).click() - cy.get(".spectrum-Picker-label").click() - cy.get(".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel").click() + cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Picker-label").click() + cy.get( + ".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel" + ).click() - //Onboarding type selector - cy.get( - ":nth-child(2) > .spectrum-Form-itemField > .spectrum-Textfield > .spectrum-Textfield-input" - ) - .first() - .type(email, { force: true }) - cy.get(".spectrum-Button--cta").click({ force: true }) + // Onboarding type selector + cy.get(".spectrum-Textfield-input") + .eq(0) + .first() + .type(email, { force: true }) + cy.get(".spectrum-Button--cta").click({ force: true }) + }) +}) + +Cypress.Commands.add("deleteUser", email => { + // Assumes user has access to Users section + cy.contains("Users", { timeout: 2000 }).click() + cy.contains(email).click() + + // Click Delete user button + cy.get(".spectrum-Button") + .contains("Delete user") + .click({ force: true }) + .then(() => { + // Confirm deletion within modal + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { + cy.get(".spectrum-Button") + .contains("Delete user") + .click({ force: true }) + }) + }) }) Cypress.Commands.add("updateUserInformation", (firstName, lastName) => { - cy.get(".user-dropdown .avatar > .icon").click({ force: true }) + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ + force: true, + }) cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { cy.get("li[data-cy='user-info']").click({ force: true }) @@ -95,9 +134,8 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => { const shouldCreateDefaultTable = typeof addDefaultTable != "boolean" ? true : addDefaultTable - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) + cy.get(`[data-cy="create-app-btn"]`, { timeout: 2000 }).click({ force: true }) // If apps already exist cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) @@ -117,7 +155,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => { cy.get(".spectrum-ButtonGroup") .contains("Create app") .click({ force: true }) - cy.wait(10000) + cy.wait(2000) }) if (shouldCreateDefaultTable) { cy.createTable("Cypress Tests", true) @@ -125,7 +163,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => { }) Cypress.Commands.add("deleteApp", name => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.wait(2000) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) .its("body") @@ -150,14 +188,15 @@ Cypress.Commands.add("deleteApp", name => { cy.get(actionEleId).within(() => { cy.contains("Manage").click({ force: true }) }) - cy.wait(1000) + cy.wait(500) // Unpublish first if needed cy.get(`[data-cy="app-status"]`).then($status => { - if ($status.text().includes("Last published")) { - cy.contains("Unpublish").click() + if ($status.text().includes("- Unpublish")) { + // Exact match for Unpublish + cy.contains("Unpublish").click({ force: true }) cy.get(".spectrum-Modal").within(() => { - cy.contains("Unpublish app").click() + cy.contains("Unpublish app").click({ force: true }) }) } }) @@ -276,16 +315,18 @@ Cypress.Commands.add("alterAppVersion", (appId, version) => { }) Cypress.Commands.add("importApp", (exportFilePath, name) => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) .its("body") .then(val => { if (val.length > 0) { cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) - cy.wait(500) } - cy.get(`[data-cy="import-app-btn"]`).click({ force: true }) + cy.wait(500) + cy.get(`[data-cy="import-app-btn"]`).click({ + force: true, + }) }) cy.get(".spectrum-Modal").within(() => { @@ -303,7 +344,7 @@ Cypress.Commands.add("importApp", (exportFilePath, name) => { cy.get(".confirm-wrap button") .should("not.be.disabled") .click({ force: true }) - cy.wait(5000) + cy.wait(3000) }) }) @@ -332,7 +373,8 @@ Cypress.Commands.add("searchForApplication", appName => { // Assumes there are no others Cypress.Commands.add("applicationInAppTable", appName => { - cy.get(".appTable").within(() => { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) + cy.get(".appTable", { timeout: 2000 }).within(() => { cy.get(".title").contains(appName).should("exist") }) }) @@ -360,7 +402,7 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => { cy.navigateToDataSection() cy.get(`[data-cy="new-table"]`).click() } - cy.wait(5000) + cy.wait(2000) cy.get(".item") .contains("Budibase DB") .click({ force: true }) @@ -368,8 +410,7 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => { cy.get(".spectrum-Button").contains("Continue").click({ force: true }) }) cy.get(".spectrum-Modal").within(() => { - cy.wait(1000) - cy.get("input").first().type(tableName).blur() + cy.get("input", { timeout: 1000 }).first().type(tableName).blur() cy.get(".spectrum-ButtonGroup").contains("Create").click() }) cy.contains(tableName).should("be.visible") @@ -451,8 +492,7 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => { .contains("Add Option") .click({ force: true }) .then(() => { - cy.wait(500) - cy.get("[placeholder='Label']").eq(i).type(i) + cy.get("[placeholder='Label']", { timeout: 500 }).eq(i).type(i) cy.get("[placeholder='Value']").eq(i).type(i) }) } @@ -464,10 +504,14 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => { // DESIGN AREA Cypress.Commands.add("addComponent", (category, component) => { if (category) { - cy.get(`[data-cy="category-${category}"]`).click({ force: true }) + cy.get(`[data-cy="category-${category}"]`, { timeout: 1000 }).click({ + force: true, + }) } if (component) { - cy.get(`[data-cy="component-${component}"]`).click({ force: true }) + cy.get(`[data-cy="component-${component}"]`, { timeout: 1000 }).click({ + force: true, + }) } cy.wait(1000) cy.location().then(loc => { @@ -496,15 +540,14 @@ Cypress.Commands.add("createScreen", (route, accessLevelLabel) => { cy.get(".spectrum-Modal").within(() => { cy.get("[data-cy='blank-screen']").click() cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(500) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.wait(500) + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Form-itemField").eq(0).type(route) - cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(1000) + cy.get(".confirm-wrap").contains("Continue").click({ force: true }) }) - cy.get(".spectrum-Modal").within(() => { + cy.get(".spectrum-Modal", { timeout: 1000 }).within(() => { if (accessLevelLabel) { cy.get(".spectrum-Picker-label").click() cy.wait(500) @@ -522,10 +565,12 @@ Cypress.Commands.add( cy.get(".spectrum-Modal").within(() => { cy.get(".item").contains("Autogenerated screens").click() cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(500) }) - cy.get(".spectrum-Modal [data-cy='data-source-modal']").within(() => { + cy.get(".spectrum-Modal [data-cy='data-source-modal']", { + timeout: 500, + }).within(() => { for (let i = 0; i < datasourceNames.length; i++) { + cy.wait(500) cy.get(".data-source-entry").contains(datasourceNames[i]).click() //Ensure the check mark is visible cy.get(".data-source-entry") @@ -574,7 +619,7 @@ Cypress.Commands.add( // NAVIGATION Cypress.Commands.add("navigateToFrontend", () => { // Clicks on Design tab and then the Home nav item - cy.wait(1000) + cy.wait(500) cy.contains("Design").click() cy.get(".spectrum-Search").type("/") cy.get(".nav-item").contains("home").click() @@ -606,11 +651,11 @@ Cypress.Commands.add("selectExternalDatasource", datasourceName => { cy.get(".add-button").click() }) // Clicks specified datasource & continue - cy.wait(1000) - cy.get(".item-list").contains(datasourceName).click() + cy.get(".item-list", { timeout: 1000 }).contains(datasourceName).click() cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Button").contains("Continue").click({ force: true }) }) + cy.wait(500) }) Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => { @@ -618,8 +663,7 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => { // 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(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".form-row") .eq(0) .within(() => { @@ -719,16 +763,18 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => { 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 }) + cy.get(".spectrum-Button", { timeout: 1000 }) + .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(".spectrum-Button", { timeout: 500 }) + .contains("Save") + .click({ force: true }) cy.get(".hierarchy-items-container") .should("contain", method) .and("contain", queryPrettyName) diff --git a/packages/builder/cypress/support/interact.js b/packages/builder/cypress/support/interact.js index f0fae61660..0b31d8a8c5 100644 --- a/packages/builder/cypress/support/interact.js +++ b/packages/builder/cypress/support/interact.js @@ -62,6 +62,7 @@ export const GLOBESTRIKE = "svg[aria-label=GlobeStrike]" export const GLOBE = "svg[aria-label=Globe]" export const UNPUBLISH_MODAL = "[data-cy=unpublish-modal]" export const CONFIRM_WRAP_BUTTON = ".confirm-wrap button" +export const DEPLOYMENT_TOP_NAV = ".deployment-top-nav" //changeAppiconAndColour export const APP_ROW_ACTION = ".app-row-actions-icon" @@ -97,13 +98,16 @@ export const ACTION_SPECTRUM_ICON = ".actions .spectrum-Icon" export const SPECTRUM_MENU_CHILD2 = ".spectrum-Menu > :nth-child(2)" export const DELETE_TABLE_CONFIRM = '[data-cy="delete-table-confirm"]' -//createUSerAndRoles +//adminAndManagement Folder export const SPECTRUM_TABLE = ".spectrum-Table" export const SPECTRUM_SIDENAV = ".spectrum-SideNav" export const SPECTRUM_TABLE_ROW = ".spectrum-Table-row" export const SPECTRUM_TABLE_CELL = ".spectrum-Table-cell" export const FIELD = ".field" export const CONTAINER = ".container" +export const REGENERATE = ".regenerate" +export const SPECTRUM_DIALOG_CONTENT = ".spectrum-Dialog-content" +export const SPECTRUM_ICON = ".spectrum-Icon" //createView export const SPECTRUM_MENU_ITEM_LABEL = ".spectrum-Menu-itemLabel" @@ -113,14 +117,17 @@ export const TOP_RIGHT_NAV = ".toprightnav" export const AREA_LABEL_REVERT = "[aria-label=Revert]" export const ROOT = ".root" -//quertLevelTransformers +//queryLevelTransformers export const SPECTRUM_TABS_ITEM = ".spectrum-Tabs-itemLabel" export const CODEMIRROR_TEXTAREA = ".CodeMirror textarea" -//renemaApplication +//renameApplication export const WRAPPER = ".wrapper" export const ERROR = ".error" export const AREA_LABEL_MORE = "[aria-label=More]" export const APP_ROW_ACTION_MENU_POPOVER = '[data-cy="app-row-actions-menu-popover"]' -export const SPECTRUM_MENU_ITEMM = ".spectrum-Menu-item" +export const SPECTRUM_MENU_ITEM = ".spectrum-Menu-item" + +//commands +export const HOME_LOGO = ".home-logo" diff --git a/packages/builder/package.json b/packages/builder/package.json index dc76aeb9c1..58c11f88a1 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.0.212-alpha.0", + "version": "1.0.219-alpha.0", "license": "GPL-3.0", "private": true, "scripts": { @@ -69,14 +69,15 @@ } }, "dependencies": { - "@budibase/bbui": "^1.0.212-alpha.0", - "@budibase/client": "^1.0.212-alpha.0", - "@budibase/frontend-core": "^1.0.212-alpha.0", - "@budibase/string-templates": "^1.0.212-alpha.0", + "@budibase/bbui": "^1.0.219-alpha.0", + "@budibase/client": "^1.0.219-alpha.0", + "@budibase/frontend-core": "^1.0.219-alpha.0", + "@budibase/string-templates": "^1.0.219-alpha.0", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", "codemirror": "^5.59.0", + "dayjs": "^1.11.2", "downloadjs": "1.4.7", "lodash": "4.17.21", "posthog-js": "1.4.5", diff --git a/packages/builder/src/analytics/IntercomClient.js b/packages/builder/src/analytics/IntercomClient.js index d4835d979d..6d5bf9e93e 100644 --- a/packages/builder/src/analytics/IntercomClient.js +++ b/packages/builder/src/analytics/IntercomClient.js @@ -53,7 +53,7 @@ export default class IntercomClient { * @returns Intercom global object */ show(user = {}) { - if (!this.initialised) return + if (!this.initialised || !user?.admin) return return window.Intercom("boot", { app_id: this.token, diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 8cbc629291..234f83d7cc 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -49,6 +49,95 @@ export const getBindableProperties = (asset, componentId) => { ] } +/** + * Gets all rest bindable data fields + */ +export const getRestBindings = () => { + const userBindings = getUserBindings() + return [...userBindings, ...getAuthBindings()] +} + +/** + * Gets all rest bindable auth fields + */ +export const getAuthBindings = () => { + let bindings = [] + const safeUser = makePropSafe("user") + const safeOAuth2 = makePropSafe("oauth2") + const safeAccessToken = makePropSafe("accessToken") + + const authBindings = [ + { + runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`, + readable: `Current User.OAuthToken`, + key: "accessToken", + }, + ] + + bindings = Object.keys(authBindings).map(key => { + const fieldBinding = authBindings[key] + return { + type: "context", + runtimeBinding: fieldBinding.runtime, + readableBinding: fieldBinding.readable, + fieldSchema: { type: "string", name: fieldBinding.key }, + providerId: "user", + } + }) + return bindings +} + +/** + * Utility - convert a key/value map to an array of custom 'context' bindings + * @param {object} valueMap Key/value pairings + * @param {string} prefix A contextual string prefix/path for a user readable binding + * @return {object[]} An array containing readable/runtime binding objects + */ +export const toBindingsArray = (valueMap, prefix) => { + if (!valueMap) { + return [] + } + return Object.keys(valueMap).reduce((acc, binding) => { + if (!binding || !valueMap[binding]) { + return acc + } + acc.push({ + type: "context", + runtimeBinding: binding, + readableBinding: `${prefix}.${binding}`, + }) + return acc + }, []) +} + +/** + * Utility - coverting a map of readable bindings to runtime + */ +export const readableToRuntimeMap = (bindings, ctx) => { + if (!bindings || !ctx) { + return {} + } + return Object.keys(ctx).reduce((acc, key) => { + let parsedQuery = readableToRuntimeBinding(bindings, ctx[key]) + acc[key] = parsedQuery + return acc + }, {}) +} + +/** + * Utility - coverting a map of runtime bindings to readable + */ +export const runtimeToReadableMap = (bindings, ctx) => { + if (!bindings || !ctx) { + return {} + } + return Object.keys(ctx).reduce((acc, key) => { + let parsedQuery = runtimeToReadableBinding(bindings, ctx[key]) + acc[key] = parsedQuery + return acc + }, {}) +} + /** * Gets the bindable properties exposed by a certain component. */ @@ -298,7 +387,6 @@ const getUserBindings = () => { providerId: "user", }) }) - return bindings } diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index cf42492c05..dd09e3356a 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -5,6 +5,7 @@ import { cloneDeep } from "lodash/fp" const initialAutomationState = { automations: [], + showTestPanel: false, blockDefinitions: { TRIGGER: [], ACTION: [], @@ -19,6 +20,17 @@ export const getAutomationStore = () => { } const automationActions = store => ({ + definitions: async () => { + const response = await API.getAutomationDefinitions() + store.update(state => { + state.blockDefinitions = { + TRIGGER: response.trigger, + ACTION: response.action, + } + return state + }) + return response + }, fetch: async () => { const responses = await Promise.all([ API.getAutomations(), @@ -109,6 +121,20 @@ const automationActions = store => ({ return state }) }, + getLogs: async ({ automationId, startDate, status, page } = {}) => { + return await API.getAutomationLogs({ + automationId, + startDate, + status, + page, + }) + }, + clearLogErrors: async ({ automationId, appId } = {}) => { + return await API.clearAutomationLogErrors({ + automationId, + appId, + }) + }, addTestDataToAutomation: data => { store.update(state => { state.selectedAutomation.addTestData(data) @@ -117,11 +143,10 @@ const automationActions = store => ({ }, addBlockToAutomation: (block, blockIdx) => { store.update(state => { - const newBlock = state.selectedAutomation.addBlock( + state.selectedBlock = state.selectedAutomation.addBlock( cloneDeep(block), blockIdx ) - state.selectedBlock = newBlock return state }) }, diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 3e58b25ff6..9c987c89d8 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -65,7 +65,7 @@ { - $automationStore.selectedAutomation.automation.showTestPanel = true + $automationStore.showTestPanel = true }} size="M">Test Details diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index a4c41c6948..291575f3f2 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -1,6 +1,4 @@
@@ -60,16 +80,13 @@
- {#if showTestStatus && testResult && testResult[0]} + {#if showTestStatus && testResult}
{testResult[0].outputs?.success || isTrigger - ? "Success" - : "Error"}{status?.message}
{/if} diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index fecd0fcc7e..b86cffb1f9 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -51,7 +51,7 @@ $automationStore.selectedAutomation?.automation, testData ) - $automationStore.selectedAutomation.automation.showTestPanel = true + $automationStore.showTestPanel = true } catch (error) { notifications.error("Error testing notification") } diff --git a/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte new file mode 100644 index 0000000000..a2eb904c94 --- /dev/null +++ b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte @@ -0,0 +1,137 @@ + + +
+ {#each blocks as block, idx} +
+ {#if block.stepId !== "LOOP"} + + {#if showParameters && showParameters[block.id]} + + {#if filteredResults?.[idx]?.outputs.iterations} +
+ +
+ +
+
+ {/if} + +
+ + +
+