Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b48b2592d5 |
@@ -5,11 +5,8 @@ BACKEND_PORT=3000
|
|||||||
WEB_PORT=3001
|
WEB_PORT=3001
|
||||||
|
|
||||||
echo "Configuring backend environment variables..."
|
echo "Configuring backend environment variables..."
|
||||||
|
|
||||||
cd packages/backend
|
cd packages/backend
|
||||||
|
|
||||||
rm -rf .env
|
rm -rf .env
|
||||||
|
|
||||||
echo "
|
echo "
|
||||||
PORT=$BACKEND_PORT
|
PORT=$BACKEND_PORT
|
||||||
WEB_APP_URL=http://localhost:$WEB_PORT
|
WEB_APP_URL=http://localhost:$WEB_PORT
|
||||||
@@ -24,34 +21,23 @@ WEBHOOK_SECRET_KEY=sample_webhook_secret_key
|
|||||||
APP_SECRET_KEY=sample_app_secret_key
|
APP_SECRET_KEY=sample_app_secret_key
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
SERVE_WEB_APP_SEPARATELY=true" >> .env
|
SERVE_WEB_APP_SEPARATELY=true" >> .env
|
||||||
|
|
||||||
echo "Installing backend dependencies..."
|
|
||||||
|
|
||||||
yarn
|
|
||||||
|
|
||||||
cd $CURRENT_DIR
|
cd $CURRENT_DIR
|
||||||
|
|
||||||
echo "Configuring web environment variables..."
|
echo "Configuring web environment variables..."
|
||||||
|
|
||||||
cd packages/web
|
cd packages/web
|
||||||
|
|
||||||
rm -rf .env
|
rm -rf .env
|
||||||
|
|
||||||
echo "
|
echo "
|
||||||
PORT=$WEB_PORT
|
PORT=$WEB_PORT
|
||||||
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
||||||
" >> .env
|
" >> .env
|
||||||
|
|
||||||
echo "Installing web dependencies..."
|
|
||||||
|
|
||||||
yarn
|
|
||||||
|
|
||||||
cd $CURRENT_DIR
|
cd $CURRENT_DIR
|
||||||
|
|
||||||
|
echo "Installing and linking dependencies..."
|
||||||
|
yarn
|
||||||
|
yarn lerna bootstrap
|
||||||
|
|
||||||
echo "Migrating database..."
|
echo "Migrating database..."
|
||||||
|
|
||||||
cd packages/backend
|
cd packages/backend
|
||||||
|
|
||||||
yarn db:migrate
|
yarn db:migrate
|
||||||
yarn db:seed:user
|
yarn db:seed:user
|
||||||
|
|
||||||
|
9
.github/workflows/backend.yml
vendored
9
.github/workflows/backend.yml
vendored
@@ -41,11 +41,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: cd packages/backend && yarn
|
||||||
working-directory: packages/backend
|
|
||||||
- name: Copy .env-example.test file to .env.test
|
- name: Copy .env-example.test file to .env.test
|
||||||
run: cp .env-example.test .env.test
|
run: cd packages/backend && cp .env-example.test .env.test
|
||||||
working-directory: packages/backend
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: yarn test:coverage
|
run: cd packages/backend && yarn test
|
||||||
working-directory: packages/backend
|
|
||||||
|
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -18,13 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: packages/backend/yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
working-directory: packages/backend
|
- run: cd packages/backend && yarn lint
|
||||||
- run: yarn lint
|
|
||||||
working-directory: packages/backend
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
start-backend-server:
|
start-backend-server:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -37,13 +35,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: packages/backend/yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||||
working-directory: packages/backend
|
- run: cd packages/backend && yarn start
|
||||||
- run: yarn start
|
|
||||||
working-directory: packages/backend
|
|
||||||
env:
|
env:
|
||||||
ENCRYPTION_KEY: sample_encryption_key
|
ENCRYPTION_KEY: sample_encryption_key
|
||||||
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
||||||
@@ -59,13 +55,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: packages/backend/yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||||
working-directory: packages/backend
|
- run: cd packages/backend && yarn start:worker
|
||||||
- run: yarn start:worker
|
|
||||||
working-directory: packages/backend
|
|
||||||
env:
|
env:
|
||||||
ENCRYPTION_KEY: sample_encryption_key
|
ENCRYPTION_KEY: sample_encryption_key
|
||||||
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
||||||
@@ -81,13 +75,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: packages/web/yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||||
working-directory: packages/web
|
- run: cd packages/web && yarn build
|
||||||
- run: yarn build
|
|
||||||
working-directory: packages/web
|
|
||||||
env:
|
env:
|
||||||
CI: false
|
CI: false
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
28
.github/workflows/playwright.yml
vendored
28
.github/workflows/playwright.yml
vendored
@@ -3,13 +3,12 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
# TODO: Add pull request after optimizing the total excecution time of the test suite.
|
pull_request:
|
||||||
# pull_request:
|
paths:
|
||||||
# paths:
|
- 'packages/backend/**'
|
||||||
# - 'packages/backend/**'
|
- 'packages/e2e-tests/**'
|
||||||
# - 'packages/e2e-tests/**'
|
- 'packages/web/**'
|
||||||
# - 'packages/web/**'
|
- '!packages/backend/src/apps/**'
|
||||||
# - '!packages/backend/src/apps/**'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -59,21 +58,13 @@ jobs:
|
|||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Install web dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn && yarn lerna bootstrap
|
||||||
working-directory: ./packages/web
|
|
||||||
- name: Install backend dependencies
|
|
||||||
run: yarn
|
|
||||||
working-directory: ./packages/backend
|
|
||||||
- name: Install e2e-tests dependencies
|
|
||||||
run: yarn
|
|
||||||
working-directory: ./packages/e2e-tests
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
working-directory: ./packages/e2e-tests
|
|
||||||
- name: Build Automatisch web
|
- name: Build Automatisch web
|
||||||
run: yarn build
|
|
||||||
working-directory: ./packages/web
|
working-directory: ./packages/web
|
||||||
|
run: yarn build
|
||||||
env:
|
env:
|
||||||
# Keep this until we clean up warnings in build processes
|
# Keep this until we clean up warnings in build processes
|
||||||
CI: false
|
CI: false
|
||||||
@@ -114,7 +105,6 @@ jobs:
|
|||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
working-directory: ./packages/e2e-tests
|
working-directory: ./packages/e2e-tests
|
||||||
env:
|
env:
|
||||||
PORT: 3000
|
|
||||||
LOGIN_EMAIL: user@automatisch.io
|
LOGIN_EMAIL: user@automatisch.io
|
||||||
LOGIN_PASSWORD: sample
|
LOGIN_PASSWORD: sample
|
||||||
BASE_URL: http://localhost:3000
|
BASE_URL: http://localhost:3000
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ logs
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
@@ -11,12 +11,10 @@ WORKDIR /automatisch
|
|||||||
# copy the app, note .dockerignore
|
# copy the app, note .dockerignore
|
||||||
COPY . /automatisch
|
COPY . /automatisch
|
||||||
|
|
||||||
RUN cd packages/web && yarn
|
RUN yarn
|
||||||
|
|
||||||
RUN cd packages/web && yarn build
|
RUN cd packages/web && yarn build
|
||||||
|
|
||||||
RUN cd packages/backend && yarn --production
|
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
rm -rf /usr/local/share/.cache/ && \
|
rm -rf /usr/local/share/.cache/ && \
|
||||||
apk del build-dependencies
|
apk del build-dependencies
|
||||||
|
13
lerna.json
Normal file
13
lerna.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"packages": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"version": "0.10.0",
|
||||||
|
"npmClient": "yarn",
|
||||||
|
"useWorkspaces": true,
|
||||||
|
"command": {
|
||||||
|
"add": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@automatisch/root",
|
||||||
|
"license": "See LICENSE file",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
||||||
|
"start:web": "lerna run --stream --scope=@*/web dev",
|
||||||
|
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
||||||
|
"build:docs": "cd ./packages/docs && yarn install && yarn build"
|
||||||
|
},
|
||||||
|
"workspaces": {
|
||||||
|
"packages": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"nohoist": [
|
||||||
|
"**/babel-loader",
|
||||||
|
"**/webpack",
|
||||||
|
"**/@automatisch/web",
|
||||||
|
"**/ajv"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.13.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"lerna": "^4.0.0",
|
||||||
|
"prettier": "^2.5.1"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
@@ -12,7 +12,6 @@
|
|||||||
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
|
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
|
||||||
"test": "APP_ENV=test vitest run",
|
"test": "APP_ENV=test vitest run",
|
||||||
"test:watch": "APP_ENV=test vitest watch",
|
"test:watch": "APP_ENV=test vitest watch",
|
||||||
"test:coverage": "yarn test --coverage",
|
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"db:create": "node ./bin/database/create.js",
|
"db:create": "node ./bin/database/create.js",
|
||||||
"db:seed:user": "node ./bin/database/seed-user.js",
|
"db:seed:user": "node ./bin/database/seed-user.js",
|
||||||
@@ -24,7 +23,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/express": "^3.10.1",
|
"@bull-board/express": "^3.10.1",
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@faker-js/faker": "^9.2.0",
|
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||||
"@sentry/node": "^7.42.0",
|
"@sentry/node": "^7.42.0",
|
||||||
@@ -38,9 +36,6 @@
|
|||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"debug": "~2.6.9",
|
"debug": "~2.6.9",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"eslint": "^8.13.0",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
@@ -66,7 +61,6 @@
|
|||||||
"pg": "^8.7.1",
|
"pg": "^8.7.1",
|
||||||
"php-serialize": "^4.0.2",
|
"php-serialize": "^4.0.2",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"prettier": "^2.5.1",
|
|
||||||
"raw-body": "^2.5.2",
|
"raw-body": "^2.5.2",
|
||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
@@ -98,11 +92,10 @@
|
|||||||
"url": "https://github.com/automatisch/automatisch/issues"
|
"url": "https://github.com/automatisch/automatisch/issues"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/coverage-v8": "^2.1.5",
|
|
||||||
"node-gyp": "^10.1.0",
|
"node-gyp": "^10.1.0",
|
||||||
"nodemon": "^2.0.13",
|
"nodemon": "^2.0.13",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"vitest": "^2.1.5"
|
"vitest": "^1.1.3"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
@@ -8,7 +8,7 @@ export default {
|
|||||||
key: 'instanceUrl',
|
key: 'instanceUrl',
|
||||||
label: 'WordPress instance URL',
|
label: 'WordPress instance URL',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: false,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
value: null,
|
value: null,
|
||||||
placeholder: null,
|
placeholder: null,
|
||||||
|
@@ -52,7 +52,7 @@ const appConfig = {
|
|||||||
isDev: appEnv === 'development',
|
isDev: appEnv === 'development',
|
||||||
isTest: appEnv === 'test',
|
isTest: appEnv === 'test',
|
||||||
isProd: appEnv === 'production',
|
isProd: appEnv === 'production',
|
||||||
version: '0.14.0',
|
version: '0.13.1',
|
||||||
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
||||||
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
||||||
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||||
|
@@ -7,7 +7,7 @@ export default async (request, response) => {
|
|||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const roleMappings = await samlAuthProvider
|
const roleMappings = await samlAuthProvider
|
||||||
.$relatedQuery('roleMappings')
|
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||||
.orderBy('remote_role_name', 'asc');
|
.orderBy('remote_role_name', 'asc');
|
||||||
|
|
||||||
renderObject(response, roleMappings);
|
renderObject(response, roleMappings);
|
||||||
|
@@ -8,14 +8,15 @@ export default async (request, response) => {
|
|||||||
.findById(samlAuthProviderId)
|
.findById(samlAuthProviderId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const roleMappings = await samlAuthProvider.updateRoleMappings(
|
const samlAuthProvidersRoleMappings =
|
||||||
roleMappingsParams(request)
|
await samlAuthProvider.updateRoleMappings(
|
||||||
|
samlAuthProvidersRoleMappingsParams(request)
|
||||||
);
|
);
|
||||||
|
|
||||||
renderObject(response, roleMappings);
|
renderObject(response, samlAuthProvidersRoleMappings);
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleMappingsParams = (request) => {
|
const samlAuthProvidersRoleMappingsParams = (request) => {
|
||||||
const roleMappings = request.body;
|
const roleMappings = request.body;
|
||||||
|
|
||||||
return roleMappings.map(({ roleId, remoteRoleName }) => ({
|
return roleMappings.map(({ roleId, remoteRoleName }) => ({
|
||||||
|
@@ -6,7 +6,7 @@ import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by
|
|||||||
import { createRole } from '../../../../../../test/factories/role.js';
|
import { createRole } from '../../../../../../test/factories/role.js';
|
||||||
import { createUser } from '../../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../../test/factories/user.js';
|
||||||
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
|
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
|
||||||
import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js';
|
import { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js';
|
||||||
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js';
|
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js';
|
||||||
import * as license from '../../../../../helpers/license.ee.js';
|
import * as license from '../../../../../helpers/license.ee.js';
|
||||||
|
|
||||||
@@ -21,12 +21,12 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
|||||||
|
|
||||||
samlAuthProvider = await createSamlAuthProvider();
|
samlAuthProvider = await createSamlAuthProvider();
|
||||||
|
|
||||||
await createRoleMapping({
|
await createSamlAuthProvidersRoleMapping({
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
remoteRoleName: 'Viewer',
|
remoteRoleName: 'Viewer',
|
||||||
});
|
});
|
||||||
|
|
||||||
await createRoleMapping({
|
await createSamlAuthProvidersRoleMapping({
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
remoteRoleName: 'Editor',
|
remoteRoleName: 'Editor',
|
||||||
});
|
});
|
||||||
@@ -64,7 +64,7 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
|||||||
|
|
||||||
it('should delete role mappings when given empty role mappings', async () => {
|
it('should delete role mappings when given empty role mappings', async () => {
|
||||||
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
|
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
|
||||||
'roleMappings'
|
'samlAuthProvidersRoleMappings'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(existingRoleMappings.length).toBe(2);
|
expect(existingRoleMappings.length).toBe(2);
|
||||||
@@ -149,4 +149,34 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
|||||||
.send(roleMappings)
|
.send(roleMappings)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not delete existing role mapping when error thrown', async () => {
|
||||||
|
const roleMappings = [
|
||||||
|
{
|
||||||
|
roleId: userRole.id,
|
||||||
|
remoteRoleName: {
|
||||||
|
invalid: 'data',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const roleMappingsBeforeRequest = await samlAuthProvider.$relatedQuery(
|
||||||
|
'samlAuthProvidersRoleMappings'
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(
|
||||||
|
`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings`
|
||||||
|
)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.send(roleMappings)
|
||||||
|
.expect(422);
|
||||||
|
|
||||||
|
const roleMappingsAfterRequest = await samlAuthProvider.$relatedQuery(
|
||||||
|
'samlAuthProvidersRoleMappings'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(roleMappingsBeforeRequest).toStrictEqual(roleMappingsAfterRequest);
|
||||||
|
expect(roleMappingsAfterRequest.length).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
|
|||||||
|
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
data: {
|
data: {
|
||||||
version: '0.14.0',
|
version: '0.13.1',
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
count: 1,
|
count: 1,
|
||||||
|
@@ -1,52 +0,0 @@
|
|||||||
export async function up(knex) {
|
|
||||||
await knex.schema.createTable('role_mappings', (table) => {
|
|
||||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
|
||||||
table
|
|
||||||
.uuid('saml_auth_provider_id')
|
|
||||||
.references('id')
|
|
||||||
.inTable('saml_auth_providers');
|
|
||||||
table.uuid('role_id').references('id').inTable('roles');
|
|
||||||
table.string('remote_role_name').notNullable();
|
|
||||||
|
|
||||||
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
|
||||||
|
|
||||||
table.timestamps(true, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingRoleMappings = await knex('saml_auth_providers_role_mappings');
|
|
||||||
|
|
||||||
if (existingRoleMappings.length) {
|
|
||||||
await knex('role_mappings').insert(existingRoleMappings);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await knex.schema.dropTable('saml_auth_providers_role_mappings');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex) {
|
|
||||||
await knex.schema.createTable(
|
|
||||||
'saml_auth_providers_role_mappings',
|
|
||||||
(table) => {
|
|
||||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
|
||||||
table
|
|
||||||
.uuid('saml_auth_provider_id')
|
|
||||||
.references('id')
|
|
||||||
.inTable('saml_auth_providers');
|
|
||||||
table.uuid('role_id').references('id').inTable('roles');
|
|
||||||
table.string('remote_role_name').notNullable();
|
|
||||||
|
|
||||||
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
|
||||||
|
|
||||||
table.timestamps(true, true);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const existingRoleMappings = await knex('role_mappings');
|
|
||||||
|
|
||||||
if (existingRoleMappings.length) {
|
|
||||||
await knex('saml_auth_providers_role_mappings').insert(
|
|
||||||
existingRoleMappings
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await knex.schema.dropTable('role_mappings');
|
|
||||||
}
|
|
@@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async (
|
|||||||
: [mappedUser.role];
|
: [mappedUser.role];
|
||||||
|
|
||||||
const samlAuthProviderRoleMapping = await samlAuthProvider
|
const samlAuthProviderRoleMapping = await samlAuthProvider
|
||||||
.$relatedQuery('roleMappings')
|
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||||
.whereIn('remote_role_name', mappedRoles)
|
.whereIn('remote_role_name', mappedRoles)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.first();
|
.first();
|
||||||
|
@@ -1,46 +0,0 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import userAbility from './user-ability.js';
|
|
||||||
|
|
||||||
describe('userAbility', () => {
|
|
||||||
it('should return PureAbility instantiated with user permissions', () => {
|
|
||||||
const user = {
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
subject: 'Flow',
|
|
||||||
action: 'read',
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
role: {
|
|
||||||
name: 'User',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ability = userAbility(user);
|
|
||||||
|
|
||||||
expect(ability.rules).toStrictEqual(user.permissions);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return permission-less PureAbility for user with no role', () => {
|
|
||||||
const user = {
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
subject: 'Flow',
|
|
||||||
action: 'read',
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
role: null,
|
|
||||||
};
|
|
||||||
const ability = userAbility(user);
|
|
||||||
|
|
||||||
expect(ability.rules).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return permission-less PureAbility for user with no permissions', () => {
|
|
||||||
const user = { permissions: null, role: { name: 'User' } };
|
|
||||||
const ability = userAbility(user);
|
|
||||||
|
|
||||||
expect(ability.rules).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,30 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"remoteRoleName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"roleId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"samlAuthProviderId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"samlAuthProviderId",
|
|
||||||
"roleId",
|
|
||||||
"remoteRoleName",
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
}
|
|
||||||
`;
|
|
@@ -1,6 +1,6 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
|
@@ -5,7 +5,7 @@ import appConfig from '../config/app.js';
|
|||||||
import axios from '../helpers/axios-with-proxy.js';
|
import axios from '../helpers/axios-with-proxy.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import Identity from './identity.ee.js';
|
import Identity from './identity.ee.js';
|
||||||
import RoleMapping from './role-mapping.ee.js';
|
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
|
||||||
|
|
||||||
class SamlAuthProvider extends Base {
|
class SamlAuthProvider extends Base {
|
||||||
static tableName = 'saml_auth_providers';
|
static tableName = 'saml_auth_providers';
|
||||||
@@ -53,12 +53,12 @@ class SamlAuthProvider extends Base {
|
|||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
roleMappings: {
|
samlAuthProvidersRoleMappings: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: RoleMapping,
|
modelClass: SamlAuthProvidersRoleMapping,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers.id',
|
from: 'saml_auth_providers.id',
|
||||||
to: 'role_mappings.saml_auth_provider_id',
|
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -133,22 +133,27 @@ class SamlAuthProvider extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateRoleMappings(roleMappings) {
|
async updateRoleMappings(roleMappings) {
|
||||||
await this.$relatedQuery('roleMappings').delete();
|
return await SamlAuthProvider.transaction(async (trx) => {
|
||||||
|
await this.$relatedQuery('samlAuthProvidersRoleMappings', trx).delete();
|
||||||
|
|
||||||
if (isEmpty(roleMappings)) {
|
if (isEmpty(roleMappings)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleMappingsData = roleMappings.map((roleMapping) => ({
|
const samlAuthProvidersRoleMappingsData = roleMappings.map(
|
||||||
...roleMapping,
|
(samlAuthProvidersRoleMapping) => ({
|
||||||
|
...samlAuthProvidersRoleMapping,
|
||||||
samlAuthProviderId: this.id,
|
samlAuthProviderId: this.id,
|
||||||
}));
|
})
|
||||||
|
|
||||||
const newRoleMappings = await RoleMapping.query().insertAndFetch(
|
|
||||||
roleMappingsData
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return newRoleMappings;
|
const samlAuthProvidersRoleMappings =
|
||||||
|
await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch(
|
||||||
|
samlAuthProvidersRoleMappingsData
|
||||||
|
);
|
||||||
|
|
||||||
|
return samlAuthProvidersRoleMappings;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,14 +1,8 @@
|
|||||||
import { vi, beforeEach, describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||||
import RoleMapping from '../models/role-mapping.ee';
|
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||||
import axios from '../helpers/axios-with-proxy.js';
|
|
||||||
import Identity from './identity.ee';
|
import Identity from './identity.ee';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import appConfig from '../config/app';
|
|
||||||
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
|
|
||||||
import { createRoleMapping } from '../../test/factories/role-mapping.js';
|
|
||||||
import { createRole } from '../../test/factories/role.js';
|
|
||||||
|
|
||||||
describe('SamlAuthProvider model', () => {
|
describe('SamlAuthProvider model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
@@ -31,12 +25,12 @@ describe('SamlAuthProvider model', () => {
|
|||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
roleMappings: {
|
samlAuthProvidersRoleMappings: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: RoleMapping,
|
modelClass: SamlAuthProvidersRoleMapping,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers.id',
|
from: 'saml_auth_providers.id',
|
||||||
to: 'role_mappings.saml_auth_provider_id',
|
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -51,181 +45,4 @@ describe('SamlAuthProvider model', () => {
|
|||||||
|
|
||||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loginUrl should return the URL of login', () => {
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
samlAuthProvider.issuer = 'sample-issuer';
|
|
||||||
|
|
||||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(samlAuthProvider.loginUrl).toStrictEqual(
|
|
||||||
'https://automatisch.io/login/saml/sample-issuer'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loginCallbackUrl should return the URL of login callback', () => {
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
samlAuthProvider.issuer = 'sample-issuer';
|
|
||||||
|
|
||||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(samlAuthProvider.loginCallBackUrl).toStrictEqual(
|
|
||||||
'https://automatisch.io/login/saml/sample-issuer/callback'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('remoteLogoutUrl should return the URL from entrypoint', () => {
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
samlAuthProvider.entryPoint = 'https://example.com/saml/logout';
|
|
||||||
|
|
||||||
expect(samlAuthProvider.remoteLogoutUrl).toStrictEqual(
|
|
||||||
'https://example.com/saml/logout'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('config should return the correct configuration object', () => {
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
|
|
||||||
samlAuthProvider.certificate = 'sample-certificate';
|
|
||||||
samlAuthProvider.signatureAlgorithm = 'sha256';
|
|
||||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
|
||||||
samlAuthProvider.issuer = 'sample-issuer';
|
|
||||||
|
|
||||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
const expectedConfig = {
|
|
||||||
callbackUrl: 'https://automatisch.io/login/saml/sample-issuer/callback',
|
|
||||||
cert: 'sample-certificate',
|
|
||||||
entryPoint: 'https://example.com/saml',
|
|
||||||
issuer: 'sample-issuer',
|
|
||||||
signatureAlgorithm: 'sha256',
|
|
||||||
logoutUrl: 'https://example.com/saml',
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(samlAuthProvider.config).toStrictEqual(expectedConfig);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('generateLogoutRequestBody should return a correctly encoded SAML logout request', () => {
|
|
||||||
vi.mock('uuid', () => ({
|
|
||||||
v4: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
|
|
||||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
|
||||||
samlAuthProvider.issuer = 'sample-issuer';
|
|
||||||
|
|
||||||
const mockUuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
||||||
uuidv4.mockReturnValue(mockUuid);
|
|
||||||
|
|
||||||
const sessionId = 'test-session-id';
|
|
||||||
|
|
||||||
const logoutRequest = samlAuthProvider.generateLogoutRequestBody(sessionId);
|
|
||||||
|
|
||||||
const expectedLogoutRequest = `
|
|
||||||
<samlp:LogoutRequest
|
|
||||||
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
|
||||||
ID="${mockUuid}"
|
|
||||||
Version="2.0"
|
|
||||||
IssueInstant="${new Date().toISOString()}"
|
|
||||||
Destination="https://example.com/saml">
|
|
||||||
|
|
||||||
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">sample-issuer</saml:Issuer>
|
|
||||||
<samlp:SessionIndex>test-session-id</samlp:SessionIndex>
|
|
||||||
</samlp:LogoutRequest>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const expectedEncodedRequest = Buffer.from(expectedLogoutRequest).toString(
|
|
||||||
'base64'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(logoutRequest).toBe(expectedEncodedRequest);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('terminateRemoteSession should send the correct POST request and return the response', async () => {
|
|
||||||
vi.mock('../helpers/axios-with-proxy.js', () => ({
|
|
||||||
default: {
|
|
||||||
post: vi.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
|
|
||||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
|
||||||
samlAuthProvider.generateLogoutRequestBody = vi
|
|
||||||
.fn()
|
|
||||||
.mockReturnValue('mockEncodedLogoutRequest');
|
|
||||||
|
|
||||||
const sessionId = 'test-session-id';
|
|
||||||
|
|
||||||
const mockResponse = { data: 'Logout Successful' };
|
|
||||||
axios.post.mockResolvedValue(mockResponse);
|
|
||||||
|
|
||||||
const response = await samlAuthProvider.terminateRemoteSession(sessionId);
|
|
||||||
|
|
||||||
expect(samlAuthProvider.generateLogoutRequestBody).toHaveBeenCalledWith(
|
|
||||||
sessionId
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(axios.post).toHaveBeenCalledWith(
|
|
||||||
'https://example.com/saml',
|
|
||||||
'SAMLRequest=mockEncodedLogoutRequest',
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response).toBe(mockResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateRoleMappings', () => {
|
|
||||||
let samlAuthProvider;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
samlAuthProvider = await createSamlAuthProvider();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove all existing role mappings', async () => {
|
|
||||||
await createRoleMapping({
|
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
|
||||||
remoteRoleName: 'Admin',
|
|
||||||
});
|
|
||||||
|
|
||||||
await createRoleMapping({
|
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
|
||||||
remoteRoleName: 'User',
|
|
||||||
});
|
|
||||||
|
|
||||||
await samlAuthProvider.updateRoleMappings([]);
|
|
||||||
|
|
||||||
const roleMappings = await samlAuthProvider.$relatedQuery('roleMappings');
|
|
||||||
expect(roleMappings).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the updated role mappings when new ones are provided', async () => {
|
|
||||||
const adminRole = await createRole({ name: 'Admin' });
|
|
||||||
const userRole = await createRole({ name: 'User' });
|
|
||||||
|
|
||||||
const newRoleMappings = [
|
|
||||||
{ remoteRoleName: 'Admin', roleId: adminRole.id },
|
|
||||||
{ remoteRoleName: 'User', roleId: userRole.id },
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = await samlAuthProvider.updateRoleMappings(newRoleMappings);
|
|
||||||
|
|
||||||
const refetchedRoleMappings = await samlAuthProvider.$relatedQuery(
|
|
||||||
'roleMappings'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toStrictEqual(refetchedRoleMappings);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import SamlAuthProvider from './saml-auth-provider.ee.js';
|
import SamlAuthProvider from './saml-auth-provider.ee.js';
|
||||||
|
|
||||||
class RoleMapping extends Base {
|
class SamlAuthProvidersRoleMapping extends Base {
|
||||||
static tableName = 'role_mappings';
|
static tableName = 'saml_auth_providers_role_mappings';
|
||||||
|
|
||||||
static jsonSchema = {
|
static jsonSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -21,11 +21,11 @@ class RoleMapping extends Base {
|
|||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: SamlAuthProvider,
|
modelClass: SamlAuthProvider,
|
||||||
join: {
|
join: {
|
||||||
from: 'role_mappings.saml_auth_provider_id',
|
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RoleMapping;
|
export default SamlAuthProvidersRoleMapping;
|
@@ -1,26 +1,28 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import RoleMapping from './role-mapping.ee';
|
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||||
import SamlAuthProvider from './saml-auth-provider.ee';
|
import SamlAuthProvider from './saml-auth-provider.ee';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
|
|
||||||
describe('RoleMapping model', () => {
|
describe('SamlAuthProvidersRoleMapping model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
expect(RoleMapping.tableName).toBe('role_mappings');
|
expect(SamlAuthProvidersRoleMapping.tableName).toBe(
|
||||||
|
'saml_auth_providers_role_mappings'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('jsonSchema should have the correct schema', () => {
|
it('jsonSchema should have the correct schema', () => {
|
||||||
expect(RoleMapping.jsonSchema).toMatchSnapshot();
|
expect(SamlAuthProvidersRoleMapping.jsonSchema).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('relationMappings should return correct associations', () => {
|
it('relationMappings should return correct associations', () => {
|
||||||
const relationMappings = RoleMapping.relationMappings();
|
const relationMappings = SamlAuthProvidersRoleMapping.relationMappings();
|
||||||
|
|
||||||
const expectedRelations = {
|
const expectedRelations = {
|
||||||
samlAuthProvider: {
|
samlAuthProvider: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: SamlAuthProvider,
|
modelClass: SamlAuthProvider,
|
||||||
join: {
|
join: {
|
||||||
from: 'role_mappings.saml_auth_provider_id',
|
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
@@ -212,10 +212,6 @@ class User extends Base {
|
|||||||
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get ability() {
|
|
||||||
return userAbility(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async authenticate(email, password) {
|
static async authenticate(email, password) {
|
||||||
const user = await User.query().findOne({
|
const user = await User.query().findOne({
|
||||||
email: email?.toLowerCase() || null,
|
email: email?.toLowerCase() || null,
|
||||||
@@ -411,7 +407,7 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startTrialPeriod() {
|
async startTrialPeriod() {
|
||||||
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,6 +583,62 @@ class User extends Base {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $beforeInsert(queryContext) {
|
||||||
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
|
this.email = this.email.toLowerCase();
|
||||||
|
await this.generateHash();
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
await this.startTrialPeriod();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $beforeUpdate(opt, queryContext) {
|
||||||
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
|
if (this.email) {
|
||||||
|
this.email = this.email.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.generateHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
async $afterInsert(queryContext) {
|
||||||
|
await super.$afterInsert(queryContext);
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
await this.$relatedQuery('usageData').insert({
|
||||||
|
userId: this.id,
|
||||||
|
consumedTaskCount: 0,
|
||||||
|
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $afterFind() {
|
||||||
|
if (await hasValidLicense()) return this;
|
||||||
|
|
||||||
|
if (Array.isArray(this.permissions)) {
|
||||||
|
this.permissions = this.permissions.filter((permission) => {
|
||||||
|
const restrictedSubjects = [
|
||||||
|
'App',
|
||||||
|
'Role',
|
||||||
|
'SamlAuthProvider',
|
||||||
|
'Config',
|
||||||
|
];
|
||||||
|
|
||||||
|
return !restrictedSubjects.includes(permission.subject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get ability() {
|
||||||
|
return userAbility(this);
|
||||||
|
}
|
||||||
|
|
||||||
can(action, subject) {
|
can(action, subject) {
|
||||||
const can = this.ability.can(action, subject);
|
const can = this.ability.can(action, subject);
|
||||||
|
|
||||||
@@ -602,68 +654,12 @@ class User extends Base {
|
|||||||
return conditionMap;
|
return conditionMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
lowercaseEmail() {
|
cannot(action, subject) {
|
||||||
if (this.email) {
|
const cannot = this.ability.cannot(action, subject);
|
||||||
this.email = this.email.toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createUsageData() {
|
if (cannot) throw new NotAuthorizedError();
|
||||||
if (appConfig.isCloud) {
|
|
||||||
return await this.$relatedQuery('usageData').insertAndFetch({
|
|
||||||
userId: this.id,
|
|
||||||
consumedTaskCount: 0,
|
|
||||||
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async omitEnterprisePermissionsWithoutValidLicense() {
|
return cannot;
|
||||||
if (await hasValidLicense()) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(this.permissions)) {
|
|
||||||
this.permissions = this.permissions.filter((permission) => {
|
|
||||||
const restrictedSubjects = [
|
|
||||||
'App',
|
|
||||||
'Role',
|
|
||||||
'SamlAuthProvider',
|
|
||||||
'Config',
|
|
||||||
];
|
|
||||||
|
|
||||||
return !restrictedSubjects.includes(permission.subject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async $beforeInsert(queryContext) {
|
|
||||||
await super.$beforeInsert(queryContext);
|
|
||||||
|
|
||||||
this.lowercaseEmail();
|
|
||||||
await this.generateHash();
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
|
||||||
this.startTrialPeriod();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async $beforeUpdate(opt, queryContext) {
|
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
|
||||||
|
|
||||||
this.lowercaseEmail();
|
|
||||||
|
|
||||||
await this.generateHash();
|
|
||||||
}
|
|
||||||
|
|
||||||
async $afterInsert(queryContext) {
|
|
||||||
await super.$afterInsert(queryContext);
|
|
||||||
|
|
||||||
await this.createUsageData();
|
|
||||||
}
|
|
||||||
|
|
||||||
async $afterFind() {
|
|
||||||
await this.omitEnterprisePermissionsWithoutValidLicense();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ const serializers = {
|
|||||||
Permission: permissionSerializer,
|
Permission: permissionSerializer,
|
||||||
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
||||||
SamlAuthProvider: samlAuthProviderSerializer,
|
SamlAuthProvider: samlAuthProviderSerializer,
|
||||||
RoleMapping: samlAuthProviderRoleMappingSerializer,
|
SamlAuthProvidersRoleMapping: samlAuthProviderRoleMappingSerializer,
|
||||||
AppAuthClient: appAuthClientSerializer,
|
AppAuthClient: appAuthClientSerializer,
|
||||||
AppConfig: appConfigSerializer,
|
AppConfig: appConfigSerializer,
|
||||||
Flow: flowSerializer,
|
Flow: flowSerializer,
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
|
||||||
import { createRole } from './role.js';
|
import { createRole } from './role.js';
|
||||||
import RoleMapping from '../../src/models/role-mapping.ee.js';
|
|
||||||
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
||||||
|
import SamlAuthProviderRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js';
|
||||||
|
|
||||||
export const createRoleMapping = async (params = {}) => {
|
export const createRoleMapping = async (params = {}) => {
|
||||||
params.roleId = params.roleId || (await createRole()).id;
|
params.roleId = params?.roleId || (await createRole()).id;
|
||||||
params.samlAuthProviderId =
|
params.samlAuthProviderId =
|
||||||
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
params?.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
||||||
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
|
|
||||||
|
|
||||||
const roleMapping = await RoleMapping.query().insertAndFetch(params);
|
params.remoteRoleName = params?.remoteRoleName || 'User';
|
||||||
|
|
||||||
return roleMapping;
|
const samlAuthProviderRoleMapping =
|
||||||
|
await SamlAuthProviderRoleMapping.query().insertAndFetch(params);
|
||||||
|
|
||||||
|
return samlAuthProviderRoleMapping;
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { createRole } from './role.js';
|
||||||
|
import SamlAuthProvidersRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js';
|
||||||
|
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
||||||
|
|
||||||
|
export const createSamlAuthProvidersRoleMapping = async (params = {}) => {
|
||||||
|
params.roleId = params.roleId || (await createRole()).id;
|
||||||
|
params.samlAuthProviderId =
|
||||||
|
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
||||||
|
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
|
||||||
|
|
||||||
|
const samlAuthProvider =
|
||||||
|
await SamlAuthProvidersRoleMapping.query().insertAndFetch(params);
|
||||||
|
|
||||||
|
return samlAuthProvider;
|
||||||
|
};
|
@@ -15,7 +15,7 @@ const getRoleMappingsMock = async (roleMappings) => {
|
|||||||
currentPage: null,
|
currentPage: null,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
totalPages: null,
|
totalPages: null,
|
||||||
type: 'RoleMapping',
|
type: 'SamlAuthProvidersRoleMapping',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -15,7 +15,7 @@ const createRoleMappingsMock = async (roleMappings) => {
|
|||||||
currentPage: null,
|
currentPage: null,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
totalPages: null,
|
totalPages: null,
|
||||||
type: 'RoleMapping',
|
type: 'SamlAuthProvidersRoleMapping',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -2,25 +2,8 @@ import { defineConfig } from 'vitest/config';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
root: './',
|
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
setupFiles: ['./test/setup/global-hooks.js'],
|
setupFiles: ['./test/setup/global-hooks.js'],
|
||||||
globals: true,
|
globals: true,
|
||||||
reporters: process.env.GITHUB_ACTIONS ? ['dot', 'github-actions'] : ['dot'],
|
|
||||||
coverage: {
|
|
||||||
reportOnFailure: true,
|
|
||||||
provider: 'v8',
|
|
||||||
reportsDirectory: './coverage',
|
|
||||||
reporter: ['text', 'lcov'],
|
|
||||||
all: true,
|
|
||||||
include: ['**/src/models/**', '**/src/controllers/**'],
|
|
||||||
thresholds: {
|
|
||||||
autoUpdate: true,
|
|
||||||
statements: 93.41,
|
|
||||||
branches: 93.46,
|
|
||||||
functions: 95.95,
|
|
||||||
lines: 93.41,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
1
packages/docs/.gitignore
vendored
1
packages/docs/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
pages/.vitepress/cache
|
|
@@ -4,7 +4,6 @@
|
|||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev pages --port 3002",
|
"dev": "vitepress dev pages --port 3002",
|
||||||
"build": "vitepress build pages",
|
"build": "vitepress build pages",
|
||||||
|
@@ -6,19 +6,11 @@ Clone main branch of Automatisch.
|
|||||||
git clone git@github.com:automatisch/automatisch.git
|
git clone git@github.com:automatisch/automatisch.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, install the dependencies for both backend and web packages separately.
|
Then, install the dependencies.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd automatisch
|
cd automatisch
|
||||||
|
|
||||||
# Install backend dependencies
|
|
||||||
cd packages/backend
|
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# Install web dependencies
|
|
||||||
cd packages/web
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Backend
|
## Backend
|
||||||
@@ -61,14 +53,12 @@ yarn db:seed:user
|
|||||||
Start the main backend server.
|
Start the main backend server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd packages/backend
|
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the worker server in another terminal tab.
|
Start the worker server in another terminal tab.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd packages/backend
|
|
||||||
yarn worker
|
yarn worker
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -94,7 +84,6 @@ It will automatically open [http://localhost:3001](http://localhost:3001) in you
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd packages/docs
|
cd packages/docs
|
||||||
yarn install
|
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# Repository Structure
|
# Repository Structure
|
||||||
|
|
||||||
We manage a monorepo structure with the following packages:
|
We use `lerna` with `yarn workspaces` to manage the mono repository. We have the following packages:
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
@@ -15,5 +15,3 @@ We manage a monorepo structure with the following packages:
|
|||||||
- `docs` - The docs package contains the documentation website.
|
- `docs` - The docs package contains the documentation website.
|
||||||
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
||||||
- `web` - The web package contains the frontend application of Automatisch.
|
- `web` - The web package contains the frontend application of Automatisch.
|
||||||
|
|
||||||
Each package is independently managed, and has its own package.json file to manage dependencies. This allows for better isolation and flexibility.
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
|||||||
const { AuthenticatedPage } = require('./authenticated-page');
|
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
export class ExecutionDetailsPage extends AuthenticatedPage {
|
|
||||||
constructor(page) {
|
|
||||||
super(page);
|
|
||||||
|
|
||||||
this.executionCreatedAt = page.getByTestId('execution-created-at');
|
|
||||||
this.executionId = page.getByTestId('execution-id');
|
|
||||||
this.executionName = page.getByTestId('execution-name');
|
|
||||||
this.executionStep = page.getByTestId('execution-step');
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyExecutionData(flowId) {
|
|
||||||
await expect(this.executionCreatedAt).toContainText(/\d+ seconds? ago/);
|
|
||||||
await expect(this.executionId).toHaveText(
|
|
||||||
/Execution ID: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
||||||
);
|
|
||||||
await expect(this.executionName).toHaveText(flowId);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,93 +0,0 @@
|
|||||||
const { ExecutionDetailsPage } = require('./execution-details-page');
|
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
export class ExecutionStepDetails extends ExecutionDetailsPage {
|
|
||||||
constructor(page, executionStep) {
|
|
||||||
super(page);
|
|
||||||
|
|
||||||
this.executionStep = executionStep;
|
|
||||||
this.stepType = executionStep.getByTestId('step-type');
|
|
||||||
this.stepPositionAndName = executionStep.getByTestId(
|
|
||||||
'step-position-and-name'
|
|
||||||
);
|
|
||||||
this.executionStepId = executionStep.getByTestId('execution-step-id');
|
|
||||||
this.executionStepExecutedAt = executionStep.getByTestId(
|
|
||||||
'execution-step-executed-at'
|
|
||||||
);
|
|
||||||
this.dataInTab = executionStep.getByTestId('data-in-tab');
|
|
||||||
this.dataInPanel = executionStep.getByTestId('data-in-panel');
|
|
||||||
this.dataOutTab = executionStep.getByTestId('data-out-tab');
|
|
||||||
this.dataOutPanel = executionStep.getByTestId('data-out-panel');
|
|
||||||
}
|
|
||||||
|
|
||||||
async expectDataInTabToBeSelectedByDefault() {
|
|
||||||
await expect(this.dataInTab).toHaveClass(/Mui-selected/);
|
|
||||||
}
|
|
||||||
|
|
||||||
async expectDataInToContainText(searchText, desiredText) {
|
|
||||||
await expect(this.dataInPanel).toContainText(desiredText);
|
|
||||||
await this.dataInPanel.locator('#search-input').fill(searchText);
|
|
||||||
await expect(this.dataInPanel).toContainText(desiredText);
|
|
||||||
}
|
|
||||||
|
|
||||||
async expectDataOutToContainText(searchText, desiredText) {
|
|
||||||
await expect(this.dataOutPanel).toContainText(desiredText);
|
|
||||||
await this.dataOutPanel.locator('#search-input').fill(searchText);
|
|
||||||
await expect(this.dataOutPanel).toContainText(desiredText);
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyTriggerExecutionStep({
|
|
||||||
stepPositionAndName,
|
|
||||||
stepDataInKey,
|
|
||||||
stepDataInValue,
|
|
||||||
stepDataOutKey,
|
|
||||||
stepDataOutValue,
|
|
||||||
}) {
|
|
||||||
await expect(this.stepType).toHaveText('Trigger');
|
|
||||||
await this.verifyExecutionStep({
|
|
||||||
stepPositionAndName,
|
|
||||||
stepDataInKey,
|
|
||||||
stepDataInValue,
|
|
||||||
stepDataOutKey,
|
|
||||||
stepDataOutValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyActionExecutionStep({
|
|
||||||
stepPositionAndName,
|
|
||||||
stepDataInKey,
|
|
||||||
stepDataInValue,
|
|
||||||
stepDataOutKey,
|
|
||||||
stepDataOutValue,
|
|
||||||
}) {
|
|
||||||
await expect(this.stepType).toHaveText('Action');
|
|
||||||
await this.verifyExecutionStep({
|
|
||||||
stepPositionAndName,
|
|
||||||
stepDataInKey,
|
|
||||||
stepDataInValue,
|
|
||||||
stepDataOutKey,
|
|
||||||
stepDataOutValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyExecutionStep({
|
|
||||||
stepPositionAndName,
|
|
||||||
stepDataInKey,
|
|
||||||
stepDataInValue,
|
|
||||||
stepDataOutKey,
|
|
||||||
stepDataOutValue,
|
|
||||||
}) {
|
|
||||||
await expect(this.stepPositionAndName).toHaveText(stepPositionAndName);
|
|
||||||
await expect(this.executionStepId).toHaveText(
|
|
||||||
/ID: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
||||||
);
|
|
||||||
await expect(this.executionStepExecutedAt).toContainText(
|
|
||||||
/executed \d+ seconds? ago/
|
|
||||||
);
|
|
||||||
await this.expectDataInTabToBeSelectedByDefault();
|
|
||||||
await this.expectDataInToContainText(stepDataInKey, stepDataInValue);
|
|
||||||
await this.dataOutTab.click();
|
|
||||||
await expect(this.dataOutPanel).toContainText(stepDataOutValue);
|
|
||||||
await this.expectDataOutToContainText(stepDataOutKey, stepDataOutValue);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,11 +2,4 @@ const { AuthenticatedPage } = require('./authenticated-page');
|
|||||||
|
|
||||||
export class ExecutionsPage extends AuthenticatedPage {
|
export class ExecutionsPage extends AuthenticatedPage {
|
||||||
screenshotPath = '/executions';
|
screenshotPath = '/executions';
|
||||||
|
|
||||||
constructor(page) {
|
|
||||||
super(page);
|
|
||||||
|
|
||||||
this.executionRow = this.page.getByTestId('execution-row');
|
|
||||||
this.executionsPageLoader = this.page.getByTestId('executions-loader');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ const { test, expect } = require('@playwright/test');
|
|||||||
const { ApplicationsPage } = require('./applications-page');
|
const { ApplicationsPage } = require('./applications-page');
|
||||||
const { ConnectionsPage } = require('./connections-page');
|
const { ConnectionsPage } = require('./connections-page');
|
||||||
const { ExecutionsPage } = require('./executions-page');
|
const { ExecutionsPage } = require('./executions-page');
|
||||||
const { ExecutionDetailsPage } = require('./execution-details-page');
|
|
||||||
const { FlowEditorPage } = require('./flow-editor-page');
|
const { FlowEditorPage } = require('./flow-editor-page');
|
||||||
const { UserInterfacePage } = require('./user-interface-page');
|
const { UserInterfacePage } = require('./user-interface-page');
|
||||||
const { LoginPage } = require('./login-page');
|
const { LoginPage } = require('./login-page');
|
||||||
@@ -30,9 +29,6 @@ exports.test = test.extend({
|
|||||||
executionsPage: async ({ page }, use) => {
|
executionsPage: async ({ page }, use) => {
|
||||||
await use(new ExecutionsPage(page));
|
await use(new ExecutionsPage(page));
|
||||||
},
|
},
|
||||||
executionDetailsPage: async ({ page }, use) => {
|
|
||||||
await use(new ExecutionDetailsPage(page));
|
|
||||||
},
|
|
||||||
flowEditorPage: async ({ page }, use) => {
|
flowEditorPage: async ({ page }, use) => {
|
||||||
await use(new FlowEditorPage(page));
|
await use(new FlowEditorPage(page));
|
||||||
},
|
},
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
const { expect } = require('../fixtures/index');
|
|
||||||
|
|
||||||
export const getToken = async (apiRequest) => {
|
|
||||||
const tokenResponse = await apiRequest.post(
|
|
||||||
`http://localhost:${process.env.PORT}/api/v1/access-tokens`,
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
email: process.env.LOGIN_EMAIL,
|
|
||||||
password: process.env.LOGIN_PASSWORD,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await expect(tokenResponse.status()).toBe(200);
|
|
||||||
return await tokenResponse.json();
|
|
||||||
};
|
|
@@ -1,108 +0,0 @@
|
|||||||
const { expect } = require('../fixtures/index');
|
|
||||||
|
|
||||||
export const createFlow = async (request, token) => {
|
|
||||||
const response = await request.post(
|
|
||||||
`http://localhost:${process.env.PORT}/api/v1/flows`,
|
|
||||||
{ headers: { Authorization: token } }
|
|
||||||
);
|
|
||||||
await expect(response.status()).toBe(201);
|
|
||||||
return await response.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFlow = async (request, token, flowId) => {
|
|
||||||
const response = await request.get(
|
|
||||||
`http://localhost:${process.env.PORT}/api/v1/flows/${flowId}`,
|
|
||||||
{ headers: { Authorization: token } }
|
|
||||||
);
|
|
||||||
await expect(response.status()).toBe(200);
|
|
||||||
return await response.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateFlowName = async (request, token, flowId) => {
|
|
||||||
const updateFlowNameResponse = await request.patch(
|
|
||||||
`http://localhost:${process.env.PORT}/api/v1/flows/${flowId}`,
|
|
||||||
{
|
|
||||||
headers: { Authorization: token },
|
|
||||||
data: { name: flowId },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await expect(updateFlowNameResponse.status()).toBe(200);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateFlowStep = async (request, token, stepId, requestBody) => {
|
|
||||||
const updateTriggerStepResponse = await request.patch(
|
|
||||||
`http://localhost:${process.env.PORT}/api/v1/steps/${stepId}`,
|
|
||||||
{
|
|
||||||
headers: { Authorization: token },
|
|
||||||
data: requestBody,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await expect(updateTriggerStepResponse.status()).toBe(200);
|
|
||||||
return await updateTriggerStepResponse.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testStep = async (request, token, stepId) => {
|
|
||||||
const testTriggerStepResponse = await request.post(
|
|
||||||
`http://localhost:${process.env.PORT}/api/v1/steps/${stepId}/test`,
|
|
||||||
{
|
|
||||||
headers: { Authorization: token },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await expect(testTriggerStepResponse.status()).toBe(200);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const publishFlow = async (request, token, flowId) => {
|
|
||||||
const publishFlowResponse = await request.patch(
|
|
||||||
`http://localhost:${process.env.PORT}/api/v1/flows/${flowId}/status`,
|
|
||||||
{
|
|
||||||
headers: { Authorization: token },
|
|
||||||
data: { active: true },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await expect(publishFlowResponse.status()).toBe(200);
|
|
||||||
return publishFlowResponse.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const triggerFlow = async (request, url) => {
|
|
||||||
const triggerFlowResponse = await request.get(url);
|
|
||||||
await expect(triggerFlowResponse.status()).toBe(204);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addWebhookFlow = async (request, token) => {
|
|
||||||
let flow = await createFlow(request, token);
|
|
||||||
const flowId = flow.data.id;
|
|
||||||
await updateFlowName(request, token, flowId);
|
|
||||||
flow = await getFlow(request, token, flowId);
|
|
||||||
const flowSteps = flow.data.steps;
|
|
||||||
|
|
||||||
const triggerStepId = flowSteps.find((step) => step.type === 'trigger').id;
|
|
||||||
const actionStepId = flowSteps.find((step) => step.type === 'action').id;
|
|
||||||
|
|
||||||
const triggerStep = await updateFlowStep(request, token, triggerStepId, {
|
|
||||||
appKey: 'webhook',
|
|
||||||
key: 'catchRawWebhook',
|
|
||||||
parameters: {
|
|
||||||
workSynchronously: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await request.get(triggerStep.data.webhookUrl);
|
|
||||||
await testStep(request, token, triggerStepId);
|
|
||||||
|
|
||||||
await updateFlowStep(request, token, actionStepId, {
|
|
||||||
appKey: 'webhook',
|
|
||||||
key: 'respondWith',
|
|
||||||
parameters: {
|
|
||||||
statusCode: '200',
|
|
||||||
body: 'ok',
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: '',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await testStep(request, token, actionStepId);
|
|
||||||
|
|
||||||
return flowId;
|
|
||||||
};
|
|
@@ -29,12 +29,10 @@
|
|||||||
"@playwright/test": "^1.45.1"
|
"@playwright/test": "^1.45.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.13.0",
|
"eslint": "^8.13.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"knex": "^2.4.0",
|
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
|
@@ -2,14 +2,11 @@ const { publicTest: setup, expect } = require('../../fixtures/index');
|
|||||||
|
|
||||||
setup.describe.serial('Admin setup page', () => {
|
setup.describe.serial('Admin setup page', () => {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
setup(
|
setup('should not be able to login if admin is not created', async ({ page, adminSetupPage, loginPage }) => {
|
||||||
'should not be able to login if admin is not created',
|
|
||||||
async ({ page, adminSetupPage }) => {
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await expect(await page.url()).toContain(adminSetupPage.path);
|
await expect(await page.url()).toContain(adminSetupPage.path);
|
||||||
}).toPass();
|
}).toPass();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
setup('should validate the inputs', async ({ adminSetupPage }) => {
|
setup('should validate the inputs', async ({ adminSetupPage }) => {
|
||||||
await adminSetupPage.open();
|
await adminSetupPage.open();
|
||||||
|
@@ -1,138 +1,37 @@
|
|||||||
const { request } = require('@playwright/test');
|
|
||||||
const { test, expect } = require('../../fixtures/index');
|
const { test, expect } = require('../../fixtures/index');
|
||||||
const {
|
|
||||||
triggerFlow,
|
|
||||||
publishFlow,
|
|
||||||
addWebhookFlow,
|
|
||||||
} = require('../../helpers/flow-api-helper');
|
|
||||||
const {
|
|
||||||
ExecutionStepDetails,
|
|
||||||
} = require('../../fixtures/execution-step-details');
|
|
||||||
const { getToken } = require('../../helpers/auth-api-helper');
|
|
||||||
|
|
||||||
test.describe('Executions page', () => {
|
|
||||||
let flowId;
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
const apiRequest = await request.newContext();
|
|
||||||
const tokenJsonResponse = await getToken(apiRequest);
|
|
||||||
|
|
||||||
flowId = await addWebhookFlow(apiRequest, tokenJsonResponse.data.token);
|
|
||||||
|
|
||||||
const { data } = await publishFlow(
|
|
||||||
apiRequest,
|
|
||||||
tokenJsonResponse.data.token,
|
|
||||||
flowId
|
|
||||||
);
|
|
||||||
|
|
||||||
const triggerStepWebhookUrl = data.steps.find(
|
|
||||||
(step) => step.type === 'trigger'
|
|
||||||
).webhookUrl;
|
|
||||||
|
|
||||||
await triggerFlow(apiRequest, triggerStepWebhookUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// no execution data exists in an empty account
|
||||||
|
test.describe.skip('Executions page', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.getByTestId('executions-page-drawer-link').click();
|
await page.getByTestId('executions-page-drawer-link').click();
|
||||||
|
await page.getByTestId('execution-row').first().click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/executions\//);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('show correct step data for trigger and actions on test and non-test execution', async ({
|
test('displays data in by default', async ({ page, executionsPage }) => {
|
||||||
page,
|
await expect(page.getByTestId('execution-step').last()).toBeVisible();
|
||||||
executionsPage,
|
await expect(page.getByTestId('execution-step')).toHaveCount(2);
|
||||||
executionDetailsPage,
|
|
||||||
}) => {
|
|
||||||
await executionsPage.executionsPageLoader.waitFor({
|
|
||||||
state: 'detached',
|
|
||||||
});
|
|
||||||
const flowExecutions = await executionsPage.executionRow.filter({
|
|
||||||
hasText: flowId,
|
|
||||||
});
|
|
||||||
await test.step('show only trigger step on test execution', async () => {
|
|
||||||
await expect(flowExecutions.last()).toContainText('Test run');
|
|
||||||
await flowExecutions.last().click();
|
|
||||||
|
|
||||||
await executionDetailsPage.verifyExecutionData(flowId);
|
await executionsPage.screenshot({
|
||||||
await expect(executionDetailsPage.executionStep).toHaveCount(1);
|
path: 'Execution - data in.png',
|
||||||
|
|
||||||
const executionStepDetails = new ExecutionStepDetails(
|
|
||||||
page,
|
|
||||||
executionDetailsPage.executionStep.last()
|
|
||||||
);
|
|
||||||
await executionStepDetails.verifyTriggerExecutionStep({
|
|
||||||
stepPositionAndName: '1. Webhook',
|
|
||||||
stepDataInKey: 'workSynchronously',
|
|
||||||
stepDataInValue: 'workSynchronously',
|
|
||||||
stepDataOutKey: 'host',
|
|
||||||
stepDataOutValue: 'localhost',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goBack();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('show trigger and action step on action test execution', async () => {
|
|
||||||
await expect(flowExecutions.nth(1)).toContainText('Test run');
|
|
||||||
await flowExecutions.nth(1).click();
|
|
||||||
|
|
||||||
await expect(executionDetailsPage.executionStep).toHaveCount(2);
|
|
||||||
await executionDetailsPage.verifyExecutionData(flowId);
|
|
||||||
|
|
||||||
const firstExecutionStepDetails = new ExecutionStepDetails(
|
|
||||||
page,
|
|
||||||
executionDetailsPage.executionStep.first()
|
|
||||||
);
|
|
||||||
await firstExecutionStepDetails.verifyTriggerExecutionStep({
|
|
||||||
stepPositionAndName: '1. Webhook',
|
|
||||||
stepDataInKey: 'workSynchronously',
|
|
||||||
stepDataInValue: 'workSynchronously',
|
|
||||||
stepDataOutKey: 'host',
|
|
||||||
stepDataOutValue: 'localhost',
|
|
||||||
});
|
|
||||||
|
|
||||||
const lastExecutionStepDetails = new ExecutionStepDetails(
|
|
||||||
page,
|
|
||||||
executionDetailsPage.executionStep.last()
|
|
||||||
);
|
|
||||||
await lastExecutionStepDetails.verifyActionExecutionStep({
|
|
||||||
stepPositionAndName: '2. Webhook',
|
|
||||||
stepDataInKey: 'body',
|
|
||||||
stepDataInValue: 'body:"ok"',
|
|
||||||
stepDataOutKey: 'body',
|
|
||||||
stepDataOutValue: 'body:"ok"',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goBack();
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('show trigger and action step on flow execution', async () => {
|
|
||||||
await expect(flowExecutions.first()).not.toContainText('Test run');
|
|
||||||
await flowExecutions.first().click();
|
|
||||||
|
|
||||||
await expect(executionDetailsPage.executionStep).toHaveCount(2);
|
|
||||||
await executionDetailsPage.verifyExecutionData(flowId);
|
|
||||||
|
|
||||||
const firstExecutionStepDetails = new ExecutionStepDetails(
|
|
||||||
page,
|
|
||||||
executionDetailsPage.executionStep.first()
|
|
||||||
);
|
|
||||||
await firstExecutionStepDetails.verifyTriggerExecutionStep({
|
|
||||||
stepPositionAndName: '1. Webhook',
|
|
||||||
stepDataInKey: 'workSynchronously',
|
|
||||||
stepDataInValue: 'workSynchronously',
|
|
||||||
stepDataOutKey: 'host',
|
|
||||||
stepDataOutValue: 'localhost',
|
|
||||||
});
|
|
||||||
|
|
||||||
const lastExecutionStepDetails = new ExecutionStepDetails(
|
|
||||||
page,
|
|
||||||
executionDetailsPage.executionStep.last()
|
|
||||||
);
|
|
||||||
await lastExecutionStepDetails.verifyActionExecutionStep({
|
|
||||||
stepPositionAndName: '2. Webhook',
|
|
||||||
stepDataInKey: 'body',
|
|
||||||
stepDataInValue: 'body:"ok"',
|
|
||||||
stepDataOutKey: 'body',
|
|
||||||
stepDataOutValue: 'body:"ok"',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('displays data out', async ({ page, executionsPage }) => {
|
||||||
|
const executionStepCount = await page.getByTestId('execution-step').count();
|
||||||
|
for (let i = 0; i < executionStepCount; i++) {
|
||||||
|
await page.getByTestId('data-out-tab').nth(i).click();
|
||||||
|
await expect(page.getByTestId('data-out-panel').nth(i)).toBeVisible();
|
||||||
|
|
||||||
|
await executionsPage.screenshot({
|
||||||
|
path: `Execution - data out - ${i}.png`,
|
||||||
|
animations: 'disabled',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not display error', async ({ page }) => {
|
||||||
|
await expect(page.getByTestId('error-tab')).toBeHidden();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,54 +1,17 @@
|
|||||||
const { request } = require('@playwright/test');
|
|
||||||
const { test, expect } = require('../../fixtures/index');
|
const { test, expect } = require('../../fixtures/index');
|
||||||
const {
|
|
||||||
triggerFlow,
|
|
||||||
publishFlow,
|
|
||||||
addWebhookFlow,
|
|
||||||
} = require('../../helpers/flow-api-helper');
|
|
||||||
const { getToken } = require('../../helpers/auth-api-helper');
|
|
||||||
|
|
||||||
test.describe('Executions page', () => {
|
test.describe('Executions page', () => {
|
||||||
let flowId;
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
|
||||||
const apiRequest = await request.newContext();
|
|
||||||
const tokenJsonResponse = await getToken(apiRequest);
|
|
||||||
|
|
||||||
flowId = await addWebhookFlow(apiRequest, tokenJsonResponse.data.token);
|
|
||||||
|
|
||||||
const { data } = await publishFlow(
|
|
||||||
apiRequest,
|
|
||||||
tokenJsonResponse.data.token,
|
|
||||||
flowId
|
|
||||||
);
|
|
||||||
|
|
||||||
const triggerStepWebhookUrl = data.steps.find(
|
|
||||||
(step) => step.type === 'trigger'
|
|
||||||
).webhookUrl;
|
|
||||||
|
|
||||||
await triggerFlow(apiRequest, triggerStepWebhookUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.getByTestId('executions-page-drawer-link').click();
|
await page.getByTestId('executions-page-drawer-link').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be able to see normal and test executions', async ({
|
// no executions exist in an empty account
|
||||||
executionsPage,
|
test.skip('displays executions', async ({ page, executionsPage }) => {
|
||||||
}) => {
|
await page.getByTestId('executions-loader').waitFor({
|
||||||
await executionsPage.executionsPageLoader.waitFor({
|
|
||||||
state: 'detached',
|
state: 'detached',
|
||||||
});
|
});
|
||||||
const flowExecutions = await executionsPage.executionRow.filter({
|
await expect(page.getByTestId('execution-row').first()).toBeVisible();
|
||||||
hasText: flowId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(flowExecutions).toHaveCount(4);
|
await executionsPage.screenshot({ path: 'Executions.png' });
|
||||||
await expect(flowExecutions.first()).toContainText('Success');
|
|
||||||
await expect(flowExecutions.first()).not.toContainText('Test run');
|
|
||||||
for (let testFlow = 1; testFlow < 4; testFlow++) {
|
|
||||||
await expect(flowExecutions.nth(testFlow)).toContainText('Test run');
|
|
||||||
await expect(flowExecutions.nth(testFlow)).toContainText('Success');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@
|
|||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
"axios": "^1.6.0",
|
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"compare-versions": "^4.1.3",
|
"compare-versions": "^4.1.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
@@ -112,7 +112,7 @@ export default function ResetPasswordForm() {
|
|||||||
<Alert
|
<Alert
|
||||||
data-test="accept-invitation-form-error"
|
data-test="accept-invitation-form-error"
|
||||||
severity="error"
|
severity="error"
|
||||||
sx={{ mt: 1 }}
|
sx={{ mt: 1, fontWeight: 500 }}
|
||||||
>
|
>
|
||||||
{formatMessage('acceptInvitationForm.invalidToken')}
|
{formatMessage('acceptInvitationForm.invalidToken')}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@@ -126,7 +126,7 @@ function AddAppConnection(props) {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
{authDocUrl && (
|
{authDocUrl && (
|
||||||
<Alert severity="info">
|
<Alert severity="info" sx={{ fontWeight: 300 }}>
|
||||||
{formatMessage('addAppConnection.callToDocs', {
|
{formatMessage('addAppConnection.callToDocs', {
|
||||||
appName: name,
|
appName: name,
|
||||||
docsLink: generateExternalLink(authDocUrl),
|
docsLink: generateExternalLink(authDocUrl),
|
||||||
@@ -138,7 +138,7 @@ function AddAppConnection(props) {
|
|||||||
<Alert
|
<Alert
|
||||||
data-test="add-connection-error"
|
data-test="add-connection-error"
|
||||||
severity="error"
|
severity="error"
|
||||||
sx={{ mt: 1, wordBreak: 'break-all' }}
|
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||||
>
|
>
|
||||||
{!errorDetails && errorMessage}
|
{!errorDetails && errorMessage}
|
||||||
{errorDetails && (
|
{errorDetails && (
|
||||||
|
@@ -32,7 +32,10 @@ function AdminApplicationAuthClientDialog(props) {
|
|||||||
<Dialog open={true} onClose={onClose}>
|
<Dialog open={true} onClose={onClose}>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
{error && (
|
{error && (
|
||||||
<Alert severity="error" sx={{ mt: 1, wordBreak: 'break-all' }}>
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||||
|
>
|
||||||
{error.message}
|
{error.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
@@ -6,7 +6,7 @@ import FormHelperText from '@mui/material/FormHelperText';
|
|||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import ClearIcon from '@mui/icons-material/Clear';
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
import { ActionButtonsWrapper } from './style';
|
import { ActionButtonsWrapper } from './style';
|
||||||
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
import { createEditor } from 'slate';
|
import { createEditor } from 'slate';
|
||||||
import { Editable, ReactEditor } from 'slate-react';
|
import { Editable, ReactEditor } from 'slate-react';
|
||||||
|
@@ -10,7 +10,7 @@ import { ExecutionPropType } from 'propTypes/propTypes';
|
|||||||
|
|
||||||
function ExecutionName(props) {
|
function ExecutionName(props) {
|
||||||
return (
|
return (
|
||||||
<Typography data-test="execution-name" variant="h3" gutterBottom>
|
<Typography variant="h3" gutterBottom>
|
||||||
{props.name}
|
{props.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@@ -29,7 +29,7 @@ function ExecutionId(props) {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<Typography data-test="execution-id" variant="body2">
|
<Typography variant="body2">
|
||||||
{formatMessage('execution.id', { id })}
|
{formatMessage('execution.id', { id })}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -47,7 +47,7 @@ function ExecutionDate(props) {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
||||||
>
|
>
|
||||||
<Typography data-test="execution-created-at" variant="body1" gutterBottom>
|
<Typography variant="body1" gutterBottom>
|
||||||
{relativeCreatedAt}
|
{relativeCreatedAt}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@@ -36,11 +36,7 @@ function ExecutionStepId(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }} gridArea="id">
|
<Box sx={{ display: 'flex' }} gridArea="id">
|
||||||
<Typography
|
<Typography variant="caption" fontWeight="bold">
|
||||||
data-test="execution-step-id"
|
|
||||||
variant="caption"
|
|
||||||
fontWeight="bold"
|
|
||||||
>
|
|
||||||
{formatMessage('executionStep.id', { id })}
|
{formatMessage('executionStep.id', { id })}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -60,11 +56,7 @@ function ExecutionStepDate(props) {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography variant="caption" gutterBottom>
|
||||||
data-test="execution-step-executed-at"
|
|
||||||
variant="caption"
|
|
||||||
gutterBottom
|
|
||||||
>
|
|
||||||
{formatMessage('executionStep.executedAt', {
|
{formatMessage('executionStep.executedAt', {
|
||||||
datetime: relativeCreatedAt,
|
datetime: relativeCreatedAt,
|
||||||
})}
|
})}
|
||||||
@@ -127,12 +119,12 @@ function ExecutionStep(props) {
|
|||||||
<ExecutionStepId id={executionStep.step.id} />
|
<ExecutionStepId id={executionStep.step.id} />
|
||||||
|
|
||||||
<Box flex="1" gridArea="step">
|
<Box flex="1" gridArea="step">
|
||||||
<Typography data-test="step-type" variant="caption">
|
<Typography variant="caption">
|
||||||
{isTrigger && formatMessage('flowStep.triggerType')}
|
{isTrigger && formatMessage('flowStep.triggerType')}
|
||||||
{isAction && formatMessage('flowStep.actionType')}
|
{isAction && formatMessage('flowStep.actionType')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography data-test="step-position-and-name" variant="body2">
|
<Typography variant="body2">
|
||||||
{step.position}. {app?.name}
|
{step.position}. {app?.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -160,7 +152,7 @@ function ExecutionStep(props) {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TabPanel value={activeTabIndex} index={0} data-test="data-in-panel">
|
<TabPanel value={activeTabIndex} index={0}>
|
||||||
<SearchableJSONViewer data={executionStep.dataIn} />
|
<SearchableJSONViewer data={executionStep.dataIn} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Alert from '@mui/material/Alert';
|
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
|
||||||
import useForgotPassword from 'hooks/useForgotPassword';
|
import useForgotPassword from 'hooks/useForgotPassword';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
@@ -12,17 +12,25 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
|||||||
export default function ForgotPasswordForm() {
|
export default function ForgotPasswordForm() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const {
|
const {
|
||||||
mutate: forgotPassword,
|
mutateAsync: forgotPassword,
|
||||||
isPending: loading,
|
isPending: loading,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useForgotPassword();
|
} = useForgotPassword();
|
||||||
|
|
||||||
const handleSubmit = ({ email }) => {
|
const handleSubmit = async (values) => {
|
||||||
forgotPassword({
|
const { email } = values;
|
||||||
|
try {
|
||||||
|
await forgotPassword({
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
enqueueSnackbar(
|
||||||
|
error?.message || formatMessage('forgotPasswordForm.error'),
|
||||||
|
{
|
||||||
|
variant: 'error',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -49,16 +57,6 @@ export default function ForgotPasswordForm() {
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
/>
|
/>
|
||||||
{isError && (
|
|
||||||
<Alert severity="error" sx={{ mt: 2 }}>
|
|
||||||
{error?.message || formatMessage('forgotPasswordForm.error')}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{isSuccess && (
|
|
||||||
<Alert severity="success" sx={{ mt: 2 }}>
|
|
||||||
{formatMessage('forgotPasswordForm.instructionsSent')}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -70,6 +68,14 @@ export default function ForgotPasswordForm() {
|
|||||||
>
|
>
|
||||||
{formatMessage('forgotPasswordForm.submit')}
|
{formatMessage('forgotPasswordForm.submit')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
{isSuccess && (
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{ color: (theme) => theme.palette.success.main }}
|
||||||
|
>
|
||||||
|
{formatMessage('forgotPasswordForm.instructionsSent')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@@ -188,7 +188,7 @@ function InstallationForm() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{install.isSuccess && (
|
{install.isSuccess && (
|
||||||
<Alert data-test="success-alert" severity="success" sx={{ mt: 3 }}>
|
<Alert data-test="success-alert" severity="success" sx={{ mt: 3, fontWeight: 500 }}>
|
||||||
{formatMessage('installationForm.success', {
|
{formatMessage('installationForm.success', {
|
||||||
link: (str) => (
|
link: (str) => (
|
||||||
<Link
|
<Link
|
||||||
|
@@ -2,7 +2,6 @@ import * as React from 'react';
|
|||||||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
import Alert from '@mui/material/Alert';
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
@@ -12,18 +11,16 @@ import Form from 'components/Form';
|
|||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useCreateAccessToken from 'hooks/useCreateAccessToken';
|
import useCreateAccessToken from 'hooks/useCreateAccessToken';
|
||||||
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
|
|
||||||
function LoginForm() {
|
function LoginForm() {
|
||||||
const isCloud = useCloud();
|
const isCloud = useCloud();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const {
|
const { mutateAsync: createAccessToken, isPending: loading } =
|
||||||
mutateAsync: createAccessToken,
|
useCreateAccessToken();
|
||||||
isPending: loading,
|
|
||||||
error,
|
|
||||||
isError,
|
|
||||||
} = useCreateAccessToken();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (authentication.isAuthenticated) {
|
if (authentication.isAuthenticated) {
|
||||||
@@ -40,19 +37,23 @@ function LoginForm() {
|
|||||||
});
|
});
|
||||||
const { token } = data;
|
const { token } = data;
|
||||||
authentication.updateToken(token);
|
authentication.updateToken(token);
|
||||||
} catch {}
|
} catch (error) {
|
||||||
};
|
const errors = error?.response?.data?.errors
|
||||||
|
? Object.values(error.response.data.errors)
|
||||||
|
: [];
|
||||||
|
|
||||||
const renderError = () => {
|
if (errors.length) {
|
||||||
const errors = error?.response?.data?.errors?.general || [
|
for (const [error] of errors) {
|
||||||
error?.message || formatMessage('loginForm.error'),
|
enqueueSnackbar(error, {
|
||||||
];
|
variant: 'error',
|
||||||
|
});
|
||||||
return errors.map((error) => (
|
}
|
||||||
<Alert severity="error" sx={{ mt: 2 }}>
|
} else {
|
||||||
{error}
|
enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
|
||||||
</Alert>
|
variant: 'error',
|
||||||
));
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -105,8 +106,6 @@ function LoginForm() {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isError && renderError()}
|
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||||
import FormHelperText from '@mui/material/FormHelperText';
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@@ -2,7 +2,6 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
|||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Alert from '@mui/material/Alert';
|
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
@@ -31,8 +30,6 @@ export default function ResetPasswordForm() {
|
|||||||
mutateAsync: resetPassword,
|
mutateAsync: resetPassword,
|
||||||
isPending,
|
isPending,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
error,
|
|
||||||
isError,
|
|
||||||
} = useResetPassword();
|
} = useResetPassword();
|
||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
@@ -50,23 +47,14 @@ export default function ResetPasswordForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
navigate(URLS.LOGIN);
|
navigate(URLS.LOGIN);
|
||||||
} catch {}
|
} catch (error) {
|
||||||
};
|
enqueueSnackbar(
|
||||||
|
|
||||||
const renderError = () => {
|
|
||||||
if (!isError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = error?.response?.data?.errors?.general || [
|
|
||||||
error?.message || formatMessage('resetPasswordForm.error'),
|
error?.message || formatMessage('resetPasswordForm.error'),
|
||||||
];
|
{
|
||||||
|
variant: 'error',
|
||||||
return errors.map((error) => (
|
},
|
||||||
<Alert severity="error" sx={{ mt: 2 }}>
|
);
|
||||||
{error}
|
}
|
||||||
</Alert>
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -108,6 +96,7 @@ export default function ResetPasswordForm() {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label={formatMessage(
|
label={formatMessage(
|
||||||
'resetPasswordForm.confirmPasswordFieldLabel',
|
'resetPasswordForm.confirmPasswordFieldLabel',
|
||||||
@@ -128,7 +117,7 @@ export default function ResetPasswordForm() {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{renderError()}
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@@ -7,7 +7,7 @@ import FormControl from '@mui/material/FormControl';
|
|||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
export default function SearchInput({ onChange, defaultValue = '' }) {
|
export default function SearchInput({ onChange }) {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
return (
|
return (
|
||||||
<FormControl variant="outlined" fullWidth>
|
<FormControl variant="outlined" fullWidth>
|
||||||
@@ -16,7 +16,6 @@ export default function SearchInput({ onChange, defaultValue = '' }) {
|
|||||||
</InputLabel>
|
</InputLabel>
|
||||||
|
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
defaultValue={defaultValue}
|
|
||||||
id="search-input"
|
id="search-input"
|
||||||
type="text"
|
type="text"
|
||||||
size="medium"
|
size="medium"
|
||||||
@@ -35,5 +34,4 @@ export default function SearchInput({ onChange, defaultValue = '' }) {
|
|||||||
|
|
||||||
SearchInput.propTypes = {
|
SearchInput.propTypes = {
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
defaultValue: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||||
import Grow from '@mui/material/Grow';
|
import Grow from '@mui/material/Grow';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import MenuList from '@mui/material/MenuList';
|
import MenuList from '@mui/material/MenuList';
|
||||||
|
@@ -84,7 +84,10 @@ function TestSubstep(props) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hasError && (
|
{hasError && (
|
||||||
<Alert severity="error" sx={{ mb: 2, width: '100%' }}>
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{ mb: 2, fontWeight: 500, width: '100%' }}
|
||||||
|
>
|
||||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
|
||||||
{JSON.stringify(errorDetails, null, 2)}
|
{JSON.stringify(errorDetails, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -101,11 +104,13 @@ function TestSubstep(props) {
|
|||||||
severity="warning"
|
severity="warning"
|
||||||
sx={{ mb: 1, width: '100%' }}
|
sx={{ mb: 1, width: '100%' }}
|
||||||
>
|
>
|
||||||
<AlertTitle>
|
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||||
{formatMessage('flowEditor.noTestDataTitle')}
|
{formatMessage('flowEditor.noTestDataTitle')}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<Box>{formatMessage('flowEditor.noTestDataMessage')}</Box>
|
<Box sx={{ fontWeight: 400 }}>
|
||||||
|
{formatMessage('flowEditor.noTestDataMessage')}
|
||||||
|
</Box>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -66,8 +66,8 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: updateRoleMappings,
|
mutateAsync: updateSamlAuthProvidersRoleMappings,
|
||||||
isPending: isUpdateRoleMappingsPending,
|
isPending: isUpdateSamlAuthProvidersRoleMappingsPending,
|
||||||
} = useAdminUpdateSamlAuthProviderRoleMappings(provider?.id);
|
} = useAdminUpdateSamlAuthProviderRoleMappings(provider?.id);
|
||||||
|
|
||||||
const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } =
|
const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } =
|
||||||
@@ -79,7 +79,7 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
const handleRoleMappingsUpdate = async (values) => {
|
const handleRoleMappingsUpdate = async (values) => {
|
||||||
try {
|
try {
|
||||||
if (provider?.id) {
|
if (provider?.id) {
|
||||||
await updateRoleMappings(
|
await updateSamlAuthProvidersRoleMappings(
|
||||||
values.roleMappings.map(({ roleId, remoteRoleName }) => ({
|
values.roleMappings.map(({ roleId, remoteRoleName }) => ({
|
||||||
roleId,
|
roleId,
|
||||||
remoteRoleName,
|
remoteRoleName,
|
||||||
@@ -148,7 +148,7 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ boxShadow: 2 }}
|
sx={{ boxShadow: 2 }}
|
||||||
loading={isUpdateRoleMappingsPending}
|
loading={isUpdateSamlAuthProvidersRoleMappingsPending}
|
||||||
>
|
>
|
||||||
{formatMessage('roleMappingsForm.save')}
|
{formatMessage('roleMappingsForm.save')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
@@ -124,6 +124,7 @@ export default function CreateUser() {
|
|||||||
<Alert
|
<Alert
|
||||||
severity="info"
|
severity="info"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
sx={{ fontWeight: '500' }}
|
||||||
data-test="invitation-email-info-alert"
|
data-test="invitation-email-info-alert"
|
||||||
>
|
>
|
||||||
{formatMessage('createUser.invitationEmailInfo', {
|
{formatMessage('createUser.invitationEmailInfo', {
|
||||||
|
@@ -42,9 +42,13 @@ export default function Execution() {
|
|||||||
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
|
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
|
||||||
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
|
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
|
||||||
<Alert severity="warning" sx={{ flex: 1 }}>
|
<Alert severity="warning" sx={{ flex: 1 }}>
|
||||||
<AlertTitle>{formatMessage('execution.noDataTitle')}</AlertTitle>
|
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||||
|
{formatMessage('execution.noDataTitle')}
|
||||||
|
</AlertTitle>
|
||||||
|
|
||||||
<Box>{formatMessage('execution.noDataMessage')}</Box>
|
<Box sx={{ fontWeight: 400 }}>
|
||||||
|
{formatMessage('execution.noDataMessage')}
|
||||||
|
</Box>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
@@ -23,18 +23,13 @@ import useLazyFlows from 'hooks/useLazyFlows';
|
|||||||
|
|
||||||
export default function Flows() {
|
export default function Flows() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||||
const flowName = searchParams.get('flowName') || '';
|
const [flowName, setFlowName] = React.useState('');
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const currentUserAbility = useCurrentUserAbility();
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
|
|
||||||
const {
|
const { data, mutate: fetchFlows } = useLazyFlows(
|
||||||
data,
|
|
||||||
mutate: fetchFlows,
|
|
||||||
isSuccess,
|
|
||||||
} = useLazyFlows(
|
|
||||||
{ flowName, page },
|
{ flowName, page },
|
||||||
{
|
{
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
@@ -43,36 +38,6 @@ export default function Flows() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const flows = data?.data || [];
|
|
||||||
const pageInfo = data?.meta;
|
|
||||||
const hasFlows = flows?.length;
|
|
||||||
const navigateToLastPage = isSuccess && !hasFlows && page > 1;
|
|
||||||
|
|
||||||
const onSearchChange = React.useCallback((event) => {
|
|
||||||
setSearchParams({ flowName: event.target.value });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getPathWithSearchParams = (page, flowName) => {
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
|
|
||||||
if (page > 1) {
|
|
||||||
searchParams.set('page', page);
|
|
||||||
}
|
|
||||||
if (flowName) {
|
|
||||||
searchParams.set('flowName', flowName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { search: searchParams.toString() };
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDuplicateFlow = () => {
|
|
||||||
if (pageInfo?.currentPage > 1) {
|
|
||||||
navigate(getPathWithSearchParams(1, flowName));
|
|
||||||
} else {
|
|
||||||
fetchFlows();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchData = React.useMemo(
|
const fetchData = React.useMemo(
|
||||||
() => debounce(fetchFlows, 300),
|
() => debounce(fetchFlows, 300),
|
||||||
[fetchFlows],
|
[fetchFlows],
|
||||||
@@ -89,14 +54,21 @@ export default function Flows() {
|
|||||||
}, [fetchData, flowName, page]);
|
}, [fetchData, flowName, page]);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function redirectToLastPage() {
|
function resetPageOnSearch() {
|
||||||
if (navigateToLastPage) {
|
// reset search params which only consists of `page`
|
||||||
navigate(getPathWithSearchParams(pageInfo.totalPages, flowName));
|
setSearchParams({});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[navigateToLastPage],
|
[flowName],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const flows = data?.data || [];
|
||||||
|
const pageInfo = data?.meta;
|
||||||
|
const hasFlows = flows?.length;
|
||||||
|
|
||||||
|
const onSearchChange = React.useCallback((event) => {
|
||||||
|
setFlowName(event.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ py: 3 }}>
|
<Box sx={{ py: 3 }}>
|
||||||
<Container>
|
<Container>
|
||||||
@@ -106,7 +78,7 @@ export default function Flows() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
||||||
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
|
<SearchInput onChange={onSearchChange} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
@@ -139,7 +111,7 @@ export default function Flows() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||||
{(isLoading || navigateToLastPage) && (
|
{isLoading && (
|
||||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
)}
|
)}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
@@ -147,11 +119,11 @@ export default function Flows() {
|
|||||||
<FlowRow
|
<FlowRow
|
||||||
key={flow.id}
|
key={flow.id}
|
||||||
flow={flow}
|
flow={flow}
|
||||||
onDuplicateFlow={onDuplicateFlow}
|
onDuplicateFlow={fetchFlows}
|
||||||
onDeleteFlow={fetchFlows}
|
onDeleteFlow={fetchFlows}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!isLoading && !navigateToLastPage && !hasFlows && (
|
{!isLoading && !hasFlows && (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
text={formatMessage('flows.noFlows')}
|
text={formatMessage('flows.noFlows')}
|
||||||
{...(currentUserAbility.can('create', 'Flow') && {
|
{...(currentUserAbility.can('create', 'Flow') && {
|
||||||
@@ -159,18 +131,18 @@ export default function Flows() {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading &&
|
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
|
||||||
!navigateToLastPage &&
|
|
||||||
pageInfo &&
|
|
||||||
pageInfo.totalPages > 1 && (
|
|
||||||
<Pagination
|
<Pagination
|
||||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||||
page={pageInfo?.currentPage}
|
page={pageInfo?.currentPage}
|
||||||
count={pageInfo?.totalPages}
|
count={pageInfo?.totalPages}
|
||||||
|
onChange={(event, page) =>
|
||||||
|
setSearchParams({ page: page.toString() })
|
||||||
|
}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => (
|
||||||
<PaginationItem
|
<PaginationItem
|
||||||
component={Link}
|
component={Link}
|
||||||
to={getPathWithSearchParams(item.page, flowName)}
|
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
|
||||||
{...item}
|
{...item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@@ -266,8 +266,8 @@ function ProfileSettings() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
<Alert variant="outlined" severity="error">
|
<Alert variant="outlined" severity="error" sx={{ fontWeight: 500 }}>
|
||||||
<AlertTitle>
|
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||||
{formatMessage('profileSettings.deleteMyAccount')}
|
{formatMessage('profileSettings.deleteMyAccount')}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
|
@@ -278,20 +278,6 @@ export const defaultTheme = createTheme({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MuiAlert: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: ({ theme }) => ({
|
|
||||||
fontWeight: theme.typography.fontWeightRegular,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiAlertTitle: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: ({ theme }) => ({
|
|
||||||
fontWeight: theme.typography.fontWeightBold,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const mationTheme = createTheme(
|
export const mationTheme = createTheme(
|
||||||
|
11069
packages/web/yarn.lock
11069
packages/web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user