Compare commits
78 Commits
hubspot-do
...
snackbar-o
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ec5ef28a7a | ||
![]() |
3d6847a3a2 | ||
![]() |
613abaec1b | ||
![]() |
b51ae9ac38 | ||
![]() |
c4fd03542b | ||
![]() |
e40d6c5ef0 | ||
![]() |
9f7dee3baa | ||
![]() |
d3da62c04a | ||
![]() |
aa7bb3f8c9 | ||
![]() |
3a9dfe339a | ||
![]() |
08830003a3 | ||
![]() |
efeeb6cb02 | ||
![]() |
ac59ce2deb | ||
![]() |
3ff89a03ac | ||
![]() |
25e231cd7c | ||
![]() |
f4d8d909b0 | ||
![]() |
35ea18a117 | ||
![]() |
ee9433261b | ||
![]() |
1cb48c7760 | ||
![]() |
172bf4bd51 | ||
![]() |
fe1039cfbc | ||
![]() |
a9de79546b | ||
![]() |
7afdf43872 | ||
![]() |
bfc7d5d0dd | ||
![]() |
690832052a | ||
![]() |
f7c1a47d52 | ||
![]() |
930843d065 | ||
![]() |
a9ee609502 | ||
![]() |
9fd2125923 | ||
![]() |
ede8703f9d | ||
![]() |
6d85623d9b | ||
![]() |
6236ee8f6d | ||
![]() |
92665d80d6 | ||
![]() |
70ae0bc77e | ||
![]() |
e28c757352 | ||
![]() |
7cdcf7ebab | ||
![]() |
7c368af5ed | ||
![]() |
df2fbbabc6 | ||
![]() |
48141eb199 | ||
![]() |
343fbb282c | ||
![]() |
cea9ed056b | ||
![]() |
73bd90c555 | ||
![]() |
917de46c45 | ||
![]() |
b592092923 | ||
![]() |
760bc1c22a | ||
![]() |
1f8b81ee78 | ||
![]() |
70af7d05e3 | ||
![]() |
14c04ee4ac | ||
![]() |
83815d3caa | ||
![]() |
487c621fa5 | ||
![]() |
304eab801c | ||
![]() |
dfe3add1cc | ||
![]() |
a32bf5539e | ||
![]() |
e944333e5f | ||
![]() |
dad23a52b0 | ||
![]() |
53606c306d | ||
![]() |
53b03f8231 | ||
![]() |
ac5f6dc024 | ||
![]() |
2c15e0dd32 | ||
![]() |
007334fef0 | ||
![]() |
b3ae2d2748 | ||
![]() |
7149c766d0 | ||
![]() |
5dca0191d2 | ||
![]() |
356668a68d | ||
![]() |
63c9442126 | ||
![]() |
0031d7911d | ||
![]() |
31c92b43b4 | ||
![]() |
2667548041 | ||
![]() |
54282ba7e0 | ||
![]() |
7f324abd44 | ||
![]() |
65a0c3b40a | ||
![]() |
2449baac5b | ||
![]() |
0ab03e1856 | ||
![]() |
9a3f85106c | ||
![]() |
42c495d8ab | ||
![]() |
58def585f1 | ||
![]() |
047034d831 | ||
![]() |
bdb2f24a81 |
@@ -28,7 +28,7 @@ cd packages/web
|
|||||||
rm -rf .env
|
rm -rf .env
|
||||||
echo "
|
echo "
|
||||||
PORT=$WEB_PORT
|
PORT=$WEB_PORT
|
||||||
REACT_APP_GRAPHQL_URL=http://localhost:$BACKEND_PORT/graphql
|
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
||||||
" >> .env
|
" >> .env
|
||||||
cd $CURRENT_DIR
|
cd $CURRENT_DIR
|
||||||
|
|
||||||
|
18
.eslintrc.js
18
.eslintrc.js
@@ -1,18 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['@typescript-eslint'],
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'prettier',
|
|
||||||
],
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['**/*.test.ts', '**/test/**/*.ts'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/ban-ts-comment': ['off'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- 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
|
||||||
- run: yarn lint
|
- run: cd packages/backend && yarn lint
|
||||||
- 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
|
||||||
|
5
.github/workflows/playwright.yml
vendored
5
.github/workflows/playwright.yml
vendored
@@ -62,8 +62,9 @@ jobs:
|
|||||||
run: yarn && yarn lerna bootstrap
|
run: yarn && yarn lerna bootstrap
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
- name: Build Automatisch
|
- name: Build Automatisch web
|
||||||
run: yarn lerna run --scope=@*/{web,cli} build
|
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
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
||||||
"start:web": "lerna run --stream --scope=@*/web dev",
|
"start:web": "lerna run --stream --scope=@*/web dev",
|
||||||
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
||||||
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend} lint",
|
|
||||||
"build:docs": "cd ./packages/docs && yarn install && yarn build"
|
"build:docs": "cd ./packages/docs && yarn install && yarn build"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
@@ -21,8 +20,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
|
||||||
"@typescript-eslint/parser": "^5.9.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",
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
"start:worker": "node src/worker.js",
|
"start:worker": "node src/worker.js",
|
||||||
"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",
|
||||||
"lint": "eslint . --ignore-path ../../.eslintignore",
|
"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",
|
||||||
"db:drop": "node ./bin/database/drop.js",
|
"db:drop": "node ./bin/database/drop.js",
|
||||||
@@ -95,7 +95,6 @@
|
|||||||
"url": "https://github.com/automatisch/automatisch/issues"
|
"url": "https://github.com/automatisch/automatisch/issues"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/utils": "^7.0.2",
|
|
||||||
"nodemon": "^2.0.13",
|
"nodemon": "^2.0.13",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"vitest": "^1.1.3"
|
"vitest": "^1.1.3"
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
import defineAction from '../../../../helpers/define-action.js';
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Get value',
|
||||||
|
key: 'getValue',
|
||||||
|
description: 'Get value from the persistent datastore.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Key',
|
||||||
|
key: 'key',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: 'The key of your value to get.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const keyValuePair = await $.datastore.get({
|
||||||
|
key: $.step.parameters.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: keyValuePair,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
4
packages/backend/src/apps/datastore/actions/index.js
Normal file
4
packages/backend/src/apps/datastore/actions/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import getValue from './get-value/index.js';
|
||||||
|
import setValue from './set-value/index.js';
|
||||||
|
|
||||||
|
export default [getValue, setValue];
|
@@ -0,0 +1,36 @@
|
|||||||
|
import defineAction from '../../../../helpers/define-action.js';
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Set value',
|
||||||
|
key: 'setValue',
|
||||||
|
description: 'Set value to the persistent datastore.',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Key',
|
||||||
|
key: 'key',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: 'The key of your value to set.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: 'The value to set.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const keyValuePair = await $.datastore.set({
|
||||||
|
key: $.step.parameters.key,
|
||||||
|
value: $.step.parameters.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: keyValuePair,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
13
packages/backend/src/apps/datastore/assets/favicon.svg
Normal file
13
packages/backend/src/apps/datastore/assets/favicon.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" id="icon">
|
||||||
|
<defs>
|
||||||
|
<style>.cls-1{fill:none;}</style>
|
||||||
|
</defs>
|
||||||
|
<title>datastore</title>
|
||||||
|
<circle cx="23" cy="23" r="1"/>
|
||||||
|
<rect x="8" y="22" width="12" height="2"/>
|
||||||
|
<circle cx="23" cy="9" r="1"/>
|
||||||
|
<rect x="8" y="8" width="12" height="2"/>
|
||||||
|
<path d="M26,14a2,2,0,0,0,2-2V6a2,2,0,0,0-2-2H6A2,2,0,0,0,4,6v6a2,2,0,0,0,2,2H8v4H6a2,2,0,0,0-2,2v6a2,2,0,0,0,2,2H26a2,2,0,0,0,2-2V20a2,2,0,0,0-2-2H24V14ZM6,6H26v6H6ZM26,26H6V20H26Zm-4-8H10V14H22Z"/>
|
||||||
|
<rect id="_Transparent_Rectangle_" data-name="<Transparent Rectangle>" class="cls-1" width="32" height="32"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 704 B |
14
packages/backend/src/apps/datastore/index.js
Normal file
14
packages/backend/src/apps/datastore/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import defineApp from '../../helpers/define-app.js';
|
||||||
|
import actions from './actions/index.js';
|
||||||
|
|
||||||
|
export default defineApp({
|
||||||
|
name: 'Datastore',
|
||||||
|
key: 'datastore',
|
||||||
|
iconUrl: '{BASE_URL}/apps/datastore/assets/favicon.svg',
|
||||||
|
authDocUrl: 'https://automatisch.io/docs/apps/datastore/connection',
|
||||||
|
supportsConnections: false,
|
||||||
|
baseUrl: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
primaryColor: '001F52',
|
||||||
|
actions,
|
||||||
|
});
|
@@ -1,4 +1,3 @@
|
|||||||
import FormData from 'form-data';
|
|
||||||
import defineAction from '../../../../helpers/define-action.js';
|
import defineAction from '../../../../helpers/define-action.js';
|
||||||
|
|
||||||
export default defineAction({
|
export default defineAction({
|
||||||
@@ -6,45 +5,51 @@ export default defineAction({
|
|||||||
key: 'newChat',
|
key: 'newChat',
|
||||||
description: 'Create a new chat session for Helix AI.',
|
description: 'Create a new chat session for Helix AI.',
|
||||||
arguments: [
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Session ID',
|
||||||
|
key: 'sessionId',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
description:
|
||||||
|
'ID of the chat session to continue. Leave empty to start a new chat.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'System Prompt',
|
||||||
|
key: 'systemPrompt',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
description:
|
||||||
|
'Optional system prompt to start the chat with. It will be used only for new chat sessions.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Input',
|
label: 'Input',
|
||||||
key: 'input',
|
key: 'input',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
description: 'Prompt to start the chat with.',
|
description: 'User input to start the chat with.',
|
||||||
variables: true,
|
variables: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
async run($) {
|
async run($) {
|
||||||
const formData = new FormData();
|
const response = await $.http.post('/api/v1/sessions/chat', {
|
||||||
formData.append('input', $.step.parameters.input);
|
session_id: $.step.parameters.sessionId,
|
||||||
formData.append('mode', 'inference');
|
system: $.step.parameters.systemPrompt,
|
||||||
formData.append('type', 'text');
|
messages: [
|
||||||
|
{
|
||||||
const sessionResponse = await $.http.post('/api/v1/sessions', formData, {
|
role: 'user',
|
||||||
headers: {
|
content: {
|
||||||
...formData.getHeaders(),
|
content_type: 'text',
|
||||||
},
|
parts: [$.step.parameters.input],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionId = sessionResponse.data.id;
|
$.setActionItem({
|
||||||
|
raw: response.data,
|
||||||
let chatGenerated = false;
|
});
|
||||||
|
|
||||||
while (!chatGenerated) {
|
|
||||||
const response = await $.http.get(`/api/v1/sessions/${sessionId}`);
|
|
||||||
|
|
||||||
const message =
|
|
||||||
response.data.interactions[response.data.interactions.length - 1];
|
|
||||||
|
|
||||||
if (message.creator === 'system' && message.state === 'complete') {
|
|
||||||
$.setActionItem({
|
|
||||||
raw: message,
|
|
||||||
});
|
|
||||||
|
|
||||||
chatGenerated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
import App from '../../../../models/app.js';
|
||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const substeps = await App.findActionSubsteps(
|
||||||
|
request.params.appKey,
|
||||||
|
request.params.actionKey
|
||||||
|
);
|
||||||
|
|
||||||
|
renderObject(response, substeps);
|
||||||
|
};
|
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import App from '../../../../models/app';
|
||||||
|
import app from '../../../../app.js';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
|
import getActionSubstepsMock from '../../../../../test/mocks/rest/api/v1/apps/get-action-substeps.js';
|
||||||
|
|
||||||
|
describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
|
||||||
|
let currentUser, exampleApp, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
exampleApp = await App.findOneByKey('github');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the app auth info', async () => {
|
||||||
|
const actions = await App.findActionsByKey('github');
|
||||||
|
const exampleAction = actions.find(
|
||||||
|
(action) => action.key === 'createIssue'
|
||||||
|
);
|
||||||
|
|
||||||
|
const endpointUrl = `/api/v1/apps/${exampleApp.key}/actions/${exampleAction.key}/substeps`;
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(endpointUrl)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = getActionSubstepsMock(exampleAction.substeps);
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
await request(app)
|
||||||
|
.get('/api/v1/apps/invalid-app-key/actions/invalid-actions-key/substeps')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for invalid action key', async () => {
|
||||||
|
const endpointUrl = `/api/v1/apps/${exampleApp.key}/actions/invalid-action-key/substeps`;
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(endpointUrl)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,8 @@
|
|||||||
|
import App from '../../../../models/app.js';
|
||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const actions = await App.findActionsByKey(request.params.appKey);
|
||||||
|
|
||||||
|
renderObject(response, actions, { serializer: 'Action' });
|
||||||
|
};
|
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import App from '../../../../models/app';
|
||||||
|
import app from '../../../../app.js';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
|
import getActionsMock from '../../../../../test/mocks/rest/api/v1/apps/get-actions.js';
|
||||||
|
|
||||||
|
describe('GET /api/v1/apps/:appKey/actions', () => {
|
||||||
|
let currentUser, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the app actions', async () => {
|
||||||
|
const exampleApp = await App.findOneByKey('github');
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/v1/apps/${exampleApp.key}/actions`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = getActionsMock(exampleApp.actions);
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
await request(app)
|
||||||
|
.get('/api/v1/apps/invalid-app-key/actions')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
});
|
16
packages/backend/src/controllers/api/v1/apps/get-apps.js
Normal file
16
packages/backend/src/controllers/api/v1/apps/get-apps.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import App from '../../../../models/app.js';
|
||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
let apps = await App.findAll(request.query.name);
|
||||||
|
|
||||||
|
if (request.query.onlyWithTriggers) {
|
||||||
|
apps = apps.filter((app) => app.triggers?.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.query.onlyWithActions) {
|
||||||
|
apps = apps.filter((app) => app.actions?.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderObject(response, apps, { serializer: 'App' });
|
||||||
|
};
|
@@ -0,0 +1,63 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import App from '../../../../models/app';
|
||||||
|
import app from '../../../../app.js';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
|
import getAppsMock from '../../../../../test/mocks/rest/api/v1/apps/get-apps.js';
|
||||||
|
|
||||||
|
describe('GET /api/v1/apps', () => {
|
||||||
|
let currentUser, apps, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
apps = await App.findAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all apps', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/v1/apps')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = getAppsMock(apps);
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all apps filtered by name', async () => {
|
||||||
|
const appsWithNameGit = apps.filter((app) => app.name.includes('Git'));
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/v1/apps?name=Git')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = getAppsMock(appsWithNameGit);
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return only the apps with triggers', async () => {
|
||||||
|
const appsWithTriggers = apps.filter((app) => app.triggers?.length > 0);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/v1/apps?onlyWithTriggers=true')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = getAppsMock(appsWithTriggers);
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return only the apps with actions', async () => {
|
||||||
|
const appsWithActions = apps.filter((app) => app.actions?.length > 0);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/v1/apps?onlyWithActions=true')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = getAppsMock(appsWithActions);
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
8
packages/backend/src/controllers/api/v1/apps/get-auth.js
Normal file
8
packages/backend/src/controllers/api/v1/apps/get-auth.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import App from '../../../../models/app.js';
|
||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const auth = await App.findAuthByKey(request.params.appKey);
|
||||||
|
|
||||||
|
renderObject(response, auth, { serializer: 'Auth' });
|
||||||
|
};
|
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import App from '../../../../models/app';
|
||||||
|
import app from '../../../../app.js';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
|
import getAuthMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth.js';
|
||||||
|
|
||||||
|
describe('GET /api/v1/apps/:appKey/auth', () => {
|
||||||
|
let currentUser, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the app auth info', async () => {
|
||||||
|
const exampleApp = await App.findOneByKey('github');
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/v1/apps/${exampleApp.key}/auth`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = getAuthMock(exampleApp.auth);
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
await request(app)
|
||||||
|
.get('/api/v1/apps/invalid-app-key/auth')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,11 @@
|
|||||||
|
import App from '../../../../models/app.js';
|
||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const substeps = await App.findTriggerSubsteps(
|
||||||
|
request.params.appKey,
|
||||||
|
request.params.triggerKey
|
||||||
|
);
|
||||||
|
|
||||||
|
renderObject(response, substeps);
|
||||||
|
};
|
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import App from '../../../../models/app';
|
||||||
|
import app from '../../../../app.js';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
|
import getTriggerSubstepsMock from '../../../../../test/mocks/rest/api/v1/apps/get-trigger-substeps.js';
|
||||||
|
|
||||||
|
describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
|
||||||
|
let currentUser, exampleApp, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
exampleApp = await App.findOneByKey('github');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the app auth info', async () => {
|
||||||
|
const triggers = await App.findTriggersByKey('github');
|
||||||
|
const exampleTrigger = triggers.find(
|
||||||
|
(trigger) => trigger.key === 'newIssues'
|
||||||
|
);
|
||||||
|
|
||||||
|
const endpointUrl = `/api/v1/apps/${exampleApp.key}/triggers/${exampleTrigger.key}/substeps`;
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(endpointUrl)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = getTriggerSubstepsMock(exampleTrigger.substeps);
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
await request(app)
|
||||||
|
.get('/api/v1/apps/invalid-app-key/triggers/invalid-trigger-key/substeps')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for invalid trigger key', async () => {
|
||||||
|
const endpointUrl = `/api/v1/apps/${exampleApp.key}/triggers/invalid-trigger-key/substeps`;
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(endpointUrl)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,8 @@
|
|||||||
|
import App from '../../../../models/app.js';
|
||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const triggers = await App.findTriggersByKey(request.params.appKey);
|
||||||
|
|
||||||
|
renderObject(response, triggers, { serializer: 'Trigger' });
|
||||||
|
};
|
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import App from '../../../../models/app';
|
||||||
|
import app from '../../../../app.js';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
|
import getTriggersMock from '../../../../../test/mocks/rest/api/v1/apps/get-triggers.js';
|
||||||
|
|
||||||
|
describe('GET /api/v1/apps/:appKey/triggers', () => {
|
||||||
|
let currentUser, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the app triggers', async () => {
|
||||||
|
const exampleApp = await App.findOneByKey('github');
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/v1/apps/${exampleApp.key}/triggers`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = getTriggersMock(exampleApp.triggers);
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
await request(app)
|
||||||
|
.get('/api/v1/apps/invalid-app-key/triggers')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
import paginateRest from '../../../../helpers/pagination-rest.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const execution = await request.currentUser.authorizedExecutions
|
||||||
|
.clone()
|
||||||
|
.withSoftDeleted()
|
||||||
|
.findById(request.params.executionId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const executionStepsQuery = execution
|
||||||
|
.$relatedQuery('executionSteps')
|
||||||
|
.withSoftDeleted()
|
||||||
|
.withGraphFetched('step')
|
||||||
|
.orderBy('created_at', 'asc');
|
||||||
|
|
||||||
|
const executionSteps = await paginateRest(
|
||||||
|
executionStepsQuery,
|
||||||
|
request.query.page
|
||||||
|
);
|
||||||
|
|
||||||
|
renderObject(response, executionSteps);
|
||||||
|
};
|
@@ -0,0 +1,153 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import Crypto from 'crypto';
|
||||||
|
import app from '../../../../app.js';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
|
import { createFlow } from '../../../../../test/factories/flow.js';
|
||||||
|
import { createStep } from '../../../../../test/factories/step.js';
|
||||||
|
import { createExecution } from '../../../../../test/factories/execution.js';
|
||||||
|
import { createExecutionStep } from '../../../../../test/factories/execution-step.js';
|
||||||
|
import { createPermission } from '../../../../../test/factories/permission';
|
||||||
|
import getExecutionStepsMock from '../../../../../test/mocks/rest/api/v1/executions/get-execution-steps';
|
||||||
|
|
||||||
|
describe('GET /api/v1/executions/:executionId/execution-steps', () => {
|
||||||
|
let currentUser, currentUserRole, anotherUser, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
currentUserRole = await currentUser.$relatedQuery('role');
|
||||||
|
|
||||||
|
anotherUser = await createUser();
|
||||||
|
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the execution steps of current user execution', async () => {
|
||||||
|
const currentUserFlow = await createFlow({
|
||||||
|
userId: currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepOne = await createStep({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
type: 'trigger',
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepTwo = await createStep({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
type: 'action',
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUserExecution = await createExecution({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUserExecutionStepOne = await createExecutionStep({
|
||||||
|
executionId: currentUserExecution.id,
|
||||||
|
stepId: stepOne.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUserExecutionStepTwo = await createExecutionStep({
|
||||||
|
executionId: currentUserExecution.id,
|
||||||
|
stepId: stepTwo.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/v1/executions/${currentUserExecution.id}/execution-steps`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = await getExecutionStepsMock(
|
||||||
|
[currentUserExecutionStepOne, currentUserExecutionStepTwo],
|
||||||
|
[stepOne, stepTwo]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the execution steps of another user execution', async () => {
|
||||||
|
const anotherUserFlow = await createFlow({
|
||||||
|
userId: anotherUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepOne = await createStep({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
type: 'trigger',
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepTwo = await createStep({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
type: 'action',
|
||||||
|
});
|
||||||
|
|
||||||
|
const anotherUserExecution = await createExecution({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const anotherUserExecutionStepOne = await createExecutionStep({
|
||||||
|
executionId: anotherUserExecution.id,
|
||||||
|
stepId: stepOne.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const anotherUserExecutionStepTwo = await createExecutionStep({
|
||||||
|
executionId: anotherUserExecution.id,
|
||||||
|
stepId: stepTwo.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/v1/executions/${anotherUserExecution.id}/execution-steps`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = await getExecutionStepsMock(
|
||||||
|
[anotherUserExecutionStepOne, anotherUserExecutionStepTwo],
|
||||||
|
[stepOne, stepTwo]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not found response for not existing execution step UUID', async () => {
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const notExistingExcecutionUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get(`/api/v1/executions/${notExistingExcecutionUUID}/execution-steps`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return bad request response for invalid UUID', async () => {
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get('/api/v1/executions/invalidExecutionUUID/execution-steps')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const execution = await request.currentUser.authorizedExecutions
|
||||||
|
.withGraphFetched({
|
||||||
|
flow: {
|
||||||
|
steps: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.withSoftDeleted()
|
||||||
|
.findById(request.params.executionId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
renderObject(response, execution);
|
||||||
|
};
|
@@ -0,0 +1,134 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import Crypto from 'crypto';
|
||||||
|
import app from '../../../../app.js';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
|
import { createFlow } from '../../../../../test/factories/flow.js';
|
||||||
|
import { createStep } from '../../../../../test/factories/step.js';
|
||||||
|
import { createExecution } from '../../../../../test/factories/execution.js';
|
||||||
|
import { createPermission } from '../../../../../test/factories/permission';
|
||||||
|
import getExecutionMock from '../../../../../test/mocks/rest/api/v1/executions/get-execution';
|
||||||
|
|
||||||
|
describe('GET /api/v1/executions/:executionId', () => {
|
||||||
|
let currentUser, currentUserRole, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
currentUserRole = await currentUser.$relatedQuery('role');
|
||||||
|
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the execution data of current user', async () => {
|
||||||
|
const currentUserFlow = await createFlow({
|
||||||
|
userId: currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepOne = await createStep({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
type: 'trigger',
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepTwo = await createStep({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
type: 'action',
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUserExecution = await createExecution({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/v1/executions/${currentUserExecution.id}`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = await getExecutionMock(
|
||||||
|
currentUserExecution,
|
||||||
|
currentUserFlow,
|
||||||
|
[stepOne, stepTwo]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the execution data of another user', async () => {
|
||||||
|
const anotherUser = await createUser();
|
||||||
|
|
||||||
|
const anotherUserFlow = await createFlow({
|
||||||
|
userId: anotherUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepOne = await createStep({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
type: 'trigger',
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepTwo = await createStep({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
type: 'action',
|
||||||
|
});
|
||||||
|
|
||||||
|
const anotherUserExecution = await createExecution({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/api/v1/executions/${anotherUserExecution.id}`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = await getExecutionMock(
|
||||||
|
anotherUserExecution,
|
||||||
|
anotherUserFlow,
|
||||||
|
[stepOne, stepTwo]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not found response for not existing execution UUID', async () => {
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const notExistingExcecutionUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get(`/api/v1/executions/${notExistingExcecutionUUID}`)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return bad request response for invalid UUID', async () => {
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get('/api/v1/executions/invalidExecutionUUID')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(400);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
import paginateRest from '../../../../helpers/pagination-rest.js';
|
||||||
|
|
||||||
|
export default async (request, response) => {
|
||||||
|
const executionsQuery = request.currentUser.authorizedExecutions
|
||||||
|
.withSoftDeleted()
|
||||||
|
.orderBy('created_at', 'desc')
|
||||||
|
.withGraphFetched({
|
||||||
|
flow: {
|
||||||
|
steps: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const executions = await paginateRest(executionsQuery, request.query.page);
|
||||||
|
|
||||||
|
for (const execution of executions.records) {
|
||||||
|
const executionSteps = await execution.$relatedQuery('executionSteps');
|
||||||
|
const status = executionSteps.some((step) => step.status === 'failure')
|
||||||
|
? 'failure'
|
||||||
|
: 'success';
|
||||||
|
|
||||||
|
execution.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderObject(response, executions);
|
||||||
|
};
|
@@ -0,0 +1,113 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import request from 'supertest';
|
||||||
|
import app from '../../../../app.js';
|
||||||
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
|
import { createFlow } from '../../../../../test/factories/flow.js';
|
||||||
|
import { createStep } from '../../../../../test/factories/step.js';
|
||||||
|
import { createExecution } from '../../../../../test/factories/execution.js';
|
||||||
|
import { createPermission } from '../../../../../test/factories/permission';
|
||||||
|
import getExecutionsMock from '../../../../../test/mocks/rest/api/v1/executions/get-executions';
|
||||||
|
|
||||||
|
describe('GET /api/v1/executions', () => {
|
||||||
|
let currentUser, currentUserRole, anotherUser, token;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await createUser();
|
||||||
|
currentUserRole = await currentUser.$relatedQuery('role');
|
||||||
|
|
||||||
|
anotherUser = await createUser();
|
||||||
|
|
||||||
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the executions of current user', async () => {
|
||||||
|
const currentUserFlow = await createFlow({
|
||||||
|
userId: currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepOne = await createStep({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
type: 'trigger',
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepTwo = await createStep({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
type: 'action',
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUserExecutionOne = await createExecution({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUserExecutionTwo = await createExecution({
|
||||||
|
flowId: currentUserFlow.id,
|
||||||
|
deletedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/v1/executions')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = await getExecutionsMock(
|
||||||
|
[currentUserExecutionTwo, currentUserExecutionOne],
|
||||||
|
currentUserFlow,
|
||||||
|
[stepOne, stepTwo]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the executions of another user', async () => {
|
||||||
|
const anotherUserFlow = await createFlow({
|
||||||
|
userId: anotherUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepOne = await createStep({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
type: 'trigger',
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepTwo = await createStep({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
type: 'action',
|
||||||
|
});
|
||||||
|
|
||||||
|
const anotherUserExecutionOne = await createExecution({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const anotherUserExecutionTwo = await createExecution({
|
||||||
|
flowId: anotherUserFlow.id,
|
||||||
|
deletedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
roleId: currentUserRole.id,
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/v1/executions')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const expectedPayload = await getExecutionsMock(
|
||||||
|
[anotherUserExecutionTwo, anotherUserExecutionOne],
|
||||||
|
anotherUserFlow,
|
||||||
|
[stepOne, stepTwo]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.body).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
@@ -2,15 +2,29 @@ import { describe, it, expect, beforeEach } from 'vitest';
|
|||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import app from '../../../../app.js';
|
import app from '../../../../app.js';
|
||||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id';
|
||||||
|
import { createPermission } from '../../../../../test/factories/permission';
|
||||||
|
import { createRole } from '../../../../../test/factories/role';
|
||||||
import { createUser } from '../../../../../test/factories/user';
|
import { createUser } from '../../../../../test/factories/user';
|
||||||
import getCurrentUserMock from '../../../../../test/mocks/rest/api/v1/users/get-current-user';
|
import getCurrentUserMock from '../../../../../test/mocks/rest/api/v1/users/get-current-user';
|
||||||
|
|
||||||
describe('GET /api/v1/users/me', () => {
|
describe('GET /api/v1/users/me', () => {
|
||||||
let role, currentUser, token;
|
let role, permissionOne, permissionTwo, currentUser, token;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
currentUser = await createUser();
|
role = await createRole();
|
||||||
role = await currentUser.$relatedQuery('role');
|
|
||||||
|
permissionOne = await createPermission({
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
permissionTwo = await createPermission({
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
currentUser = await createUser({
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
|
||||||
token = createAuthTokenByUserId(currentUser.id);
|
token = createAuthTokenByUserId(currentUser.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,7 +34,11 @@ describe('GET /api/v1/users/me', () => {
|
|||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getCurrentUserMock(currentUser, role);
|
const expectedPayload = getCurrentUserMock(currentUser, role, [
|
||||||
|
permissionOne,
|
||||||
|
permissionTwo,
|
||||||
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
export async function up(knex) {
|
||||||
|
return knex.schema.createTable('datastore', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table.string('key').notNullable();
|
||||||
|
table.string('value');
|
||||||
|
table.string('scope').notNullable();
|
||||||
|
table.uuid('scope_id').notNullable();
|
||||||
|
table.index(['key', 'scope', 'scope_id']);
|
||||||
|
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex) {
|
||||||
|
return knex.schema.dropTable('datastore');
|
||||||
|
}
|
@@ -1,21 +0,0 @@
|
|||||||
import appConfig from '../../config/app.js';
|
|
||||||
import { getLicense } from '../../helpers/license.ee.js';
|
|
||||||
|
|
||||||
const getAutomatischInfo = async () => {
|
|
||||||
const license = await getLicense();
|
|
||||||
|
|
||||||
const computedLicense = {
|
|
||||||
id: license ? license.id : null,
|
|
||||||
name: license ? license.name : null,
|
|
||||||
expireAt: license ? license.expireAt : null,
|
|
||||||
verified: license ? true : false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
isCloud: appConfig.isCloud,
|
|
||||||
isMation: appConfig.isMation,
|
|
||||||
license: computedLicense,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getAutomatischInfo;
|
|
@@ -1,190 +0,0 @@
|
|||||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import request from 'supertest';
|
|
||||||
import app from '../../app';
|
|
||||||
import * as license from '../../helpers/license.ee';
|
|
||||||
import appConfig from '../../config/app';
|
|
||||||
|
|
||||||
describe('graphQL getAutomatischInfo query', () => {
|
|
||||||
const query = `
|
|
||||||
query {
|
|
||||||
getAutomatischInfo {
|
|
||||||
isCloud
|
|
||||||
isMation
|
|
||||||
license {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
expireAt
|
|
||||||
verified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
describe('and without valid license', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.spyOn(license, 'getLicense').mockResolvedValue(false);
|
|
||||||
|
|
||||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
|
||||||
vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty license data', async () => {
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
|
||||||
data: {
|
|
||||||
getAutomatischInfo: {
|
|
||||||
isCloud: false,
|
|
||||||
isMation: false,
|
|
||||||
license: {
|
|
||||||
id: null,
|
|
||||||
name: null,
|
|
||||||
expireAt: null,
|
|
||||||
verified: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and with valid license', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
const mockedLicense = {
|
|
||||||
id: '123123',
|
|
||||||
name: 'Test License',
|
|
||||||
expireAt: '2025-08-09T10:56:54.144Z',
|
|
||||||
verified: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.spyOn(license, 'getLicense').mockResolvedValue(mockedLicense);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and with cloud flag enabled', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all license data', async () => {
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
|
||||||
data: {
|
|
||||||
getAutomatischInfo: {
|
|
||||||
isCloud: true,
|
|
||||||
isMation: false,
|
|
||||||
license: {
|
|
||||||
expireAt: '2025-08-09T10:56:54.144Z',
|
|
||||||
id: '123123',
|
|
||||||
name: 'Test License',
|
|
||||||
verified: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and with cloud flag disabled', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all license data', async () => {
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
|
||||||
data: {
|
|
||||||
getAutomatischInfo: {
|
|
||||||
isCloud: false,
|
|
||||||
isMation: false,
|
|
||||||
license: {
|
|
||||||
expireAt: '2025-08-09T10:56:54.144Z',
|
|
||||||
id: '123123',
|
|
||||||
name: 'Test License',
|
|
||||||
verified: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and with mation flag enabled', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
|
||||||
vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all license data', async () => {
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
|
||||||
data: {
|
|
||||||
getAutomatischInfo: {
|
|
||||||
isCloud: false,
|
|
||||||
isMation: true,
|
|
||||||
license: {
|
|
||||||
expireAt: '2025-08-09T10:56:54.144Z',
|
|
||||||
id: '123123',
|
|
||||||
name: 'Test License',
|
|
||||||
verified: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('and with mation flag disabled', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
|
||||||
vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return all license data', async () => {
|
|
||||||
const response = await request(app)
|
|
||||||
.post('/graphql')
|
|
||||||
.send({ query })
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const expectedResponsePayload = {
|
|
||||||
data: {
|
|
||||||
getAutomatischInfo: {
|
|
||||||
isMation: false,
|
|
||||||
isCloud: false,
|
|
||||||
license: {
|
|
||||||
expireAt: '2025-08-09T10:56:54.144Z',
|
|
||||||
id: '123123',
|
|
||||||
name: 'Test License',
|
|
||||||
verified: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -3,7 +3,6 @@ import getAppAuthClient from './queries/get-app-auth-client.ee.js';
|
|||||||
import getAppAuthClients from './queries/get-app-auth-clients.ee.js';
|
import getAppAuthClients from './queries/get-app-auth-clients.ee.js';
|
||||||
import getAppConfig from './queries/get-app-config.ee.js';
|
import getAppConfig from './queries/get-app-config.ee.js';
|
||||||
import getApps from './queries/get-apps.js';
|
import getApps from './queries/get-apps.js';
|
||||||
import getAutomatischInfo from './queries/get-automatisch-info.js';
|
|
||||||
import getBillingAndUsage from './queries/get-billing-and-usage.ee.js';
|
import getBillingAndUsage from './queries/get-billing-and-usage.ee.js';
|
||||||
import getConfig from './queries/get-config.ee.js';
|
import getConfig from './queries/get-config.ee.js';
|
||||||
import getConnectedApps from './queries/get-connected-apps.js';
|
import getConnectedApps from './queries/get-connected-apps.js';
|
||||||
@@ -39,7 +38,6 @@ const queryResolvers = {
|
|||||||
getAppAuthClients,
|
getAppAuthClients,
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
getApps,
|
getApps,
|
||||||
getAutomatischInfo,
|
|
||||||
getBillingAndUsage,
|
getBillingAndUsage,
|
||||||
getConfig,
|
getConfig,
|
||||||
getConnectedApps,
|
getConnectedApps,
|
||||||
|
@@ -40,7 +40,6 @@ type Query {
|
|||||||
key: String!
|
key: String!
|
||||||
parameters: JSONObject
|
parameters: JSONObject
|
||||||
): [SubstepArgument]
|
): [SubstepArgument]
|
||||||
getAutomatischInfo: GetAutomatischInfo
|
|
||||||
getBillingAndUsage: GetBillingAndUsage
|
getBillingAndUsage: GetBillingAndUsage
|
||||||
getCurrentUser: User
|
getCurrentUser: User
|
||||||
getConfig(keys: [String]): JSONObject
|
getConfig(keys: [String]): JSONObject
|
||||||
@@ -644,12 +643,6 @@ type AppHealth {
|
|||||||
version: String
|
version: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetAutomatischInfo {
|
|
||||||
isCloud: Boolean
|
|
||||||
isMation: Boolean
|
|
||||||
license: License
|
|
||||||
}
|
|
||||||
|
|
||||||
type License {
|
type License {
|
||||||
id: String
|
id: String
|
||||||
name: String
|
name: String
|
||||||
|
@@ -42,7 +42,6 @@ const isAuthenticatedRule = rule()(isAuthenticated);
|
|||||||
export const authenticationRules = {
|
export const authenticationRules = {
|
||||||
Query: {
|
Query: {
|
||||||
'*': isAuthenticatedRule,
|
'*': isAuthenticatedRule,
|
||||||
getAutomatischInfo: allow,
|
|
||||||
getConfig: allow,
|
getConfig: allow,
|
||||||
getNotifications: allow,
|
getNotifications: allow,
|
||||||
healthcheck: allow,
|
healthcheck: allow,
|
||||||
|
@@ -11,6 +11,18 @@ const authorizationList = {
|
|||||||
action: 'read',
|
action: 'read',
|
||||||
subject: 'Flow',
|
subject: 'Flow',
|
||||||
},
|
},
|
||||||
|
'GET /api/v1/executions/:executionId': {
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
},
|
||||||
|
'GET /api/v1/executions/': {
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
},
|
||||||
|
'GET /api/v1/executions/:executionId/execution-steps': {
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Execution',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authorizeUser = async (request, response, next) => {
|
export const authorizeUser = async (request, response, next) => {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import logger from './logger.js';
|
import logger from './logger.js';
|
||||||
import objection from 'objection';
|
import objection from 'objection';
|
||||||
|
import * as Sentry from './sentry.ee.js';
|
||||||
const { NotFoundError, DataError } = objection;
|
const { NotFoundError, DataError } = objection;
|
||||||
|
|
||||||
// Do not remove `next` argument as the function signature will not fit for an error handler middleware
|
// Do not remove `next` argument as the function signature will not fit for an error handler middleware
|
||||||
@@ -17,8 +18,21 @@ const errorHandler = (error, request, response, next) => {
|
|||||||
response.status(400).end();
|
response.status(400).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(error.message + '\n' + error.stack);
|
const statusCode = error.statusCode || 500;
|
||||||
response.status(error.statusCode || 500).end();
|
|
||||||
|
logger.error(request.method + ' ' + request.url + ' ' + statusCode);
|
||||||
|
logger.error(error.stack);
|
||||||
|
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: { rest: true },
|
||||||
|
extra: {
|
||||||
|
url: request?.url,
|
||||||
|
method: request?.method,
|
||||||
|
params: request?.params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
response.status(statusCode).end();
|
||||||
};
|
};
|
||||||
|
|
||||||
const notFoundAppError = (error) => {
|
const notFoundAppError = (error) => {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import createHttpClient from './http-client/index.js';
|
import createHttpClient from './http-client/index.js';
|
||||||
import EarlyExitError from '../errors/early-exit.js';
|
import EarlyExitError from '../errors/early-exit.js';
|
||||||
import AlreadyProcessedError from '../errors/already-processed.js';
|
import AlreadyProcessedError from '../errors/already-processed.js';
|
||||||
|
import Datastore from '../models/datastore.js';
|
||||||
|
|
||||||
const globalVariable = async (options) => {
|
const globalVariable = async (options) => {
|
||||||
const {
|
const {
|
||||||
@@ -88,6 +89,43 @@ const globalVariable = async (options) => {
|
|||||||
setActionItem: (actionItem) => {
|
setActionItem: (actionItem) => {
|
||||||
$.actionOutput.data = actionItem;
|
$.actionOutput.data = actionItem;
|
||||||
},
|
},
|
||||||
|
datastore: {
|
||||||
|
get: async ({ key }) => {
|
||||||
|
const datastore = await Datastore.query().findOne({
|
||||||
|
key,
|
||||||
|
scope: 'flow',
|
||||||
|
scope_id: $.flow.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: datastore.key,
|
||||||
|
value: datastore.value,
|
||||||
|
[datastore.key]: datastore.value,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
set: async ({ key, value }) => {
|
||||||
|
let datastore = await Datastore.query()
|
||||||
|
.where({ key, scope: 'flow', scope_id: $.flow.id })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (datastore) {
|
||||||
|
await datastore.$query().patchAndFetch({ value: value });
|
||||||
|
} else {
|
||||||
|
datastore = await Datastore.query().insert({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
scope: 'flow',
|
||||||
|
scopeId: $.flow.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: datastore.key,
|
||||||
|
value: datastore.value,
|
||||||
|
[datastore.key]: datastore.value,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (request) {
|
if (request) {
|
||||||
|
@@ -3,10 +3,13 @@ import * as Tracing from '@sentry/tracing';
|
|||||||
|
|
||||||
import appConfig from '../config/app.js';
|
import appConfig from '../config/app.js';
|
||||||
|
|
||||||
const isSentryEnabled = !!appConfig.sentryDsn;
|
const isSentryEnabled = () => {
|
||||||
|
if (appConfig.isDev || appConfig.isTest) return false;
|
||||||
|
return !!appConfig.sentryDsn;
|
||||||
|
};
|
||||||
|
|
||||||
export function init(app) {
|
export function init(app) {
|
||||||
if (!isSentryEnabled) return;
|
if (!isSentryEnabled()) return;
|
||||||
|
|
||||||
return Sentry.init({
|
return Sentry.init({
|
||||||
enabled: !!appConfig.sentryDsn,
|
enabled: !!appConfig.sentryDsn,
|
||||||
@@ -21,19 +24,19 @@ export function init(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function attachRequestHandler(app) {
|
export function attachRequestHandler(app) {
|
||||||
if (!isSentryEnabled) return;
|
if (!isSentryEnabled()) return;
|
||||||
|
|
||||||
app.use(Sentry.Handlers.requestHandler());
|
app.use(Sentry.Handlers.requestHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function attachTracingHandler(app) {
|
export function attachTracingHandler(app) {
|
||||||
if (!isSentryEnabled) return;
|
if (!isSentryEnabled()) return;
|
||||||
|
|
||||||
app.use(Sentry.Handlers.tracingHandler());
|
app.use(Sentry.Handlers.tracingHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function attachErrorHandler(app) {
|
export function attachErrorHandler(app) {
|
||||||
if (!isSentryEnabled) return;
|
if (!isSentryEnabled()) return;
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
Sentry.Handlers.errorHandler({
|
Sentry.Handlers.errorHandler({
|
||||||
@@ -46,7 +49,7 @@ export function attachErrorHandler(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function captureException(exception, captureContext) {
|
export function captureException(exception, captureContext) {
|
||||||
if (!isSentryEnabled) return;
|
if (!isSentryEnabled()) return;
|
||||||
|
|
||||||
return Sentry.captureException(exception, captureContext);
|
return Sentry.captureException(exception, captureContext);
|
||||||
}
|
}
|
||||||
|
@@ -39,6 +39,47 @@ class App {
|
|||||||
return appInfoConverter(rawAppData);
|
return appInfoConverter(rawAppData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async findAuthByKey(key, stripFuncs = false) {
|
||||||
|
const rawAppData = await getApp(key, stripFuncs);
|
||||||
|
const appData = appInfoConverter(rawAppData);
|
||||||
|
|
||||||
|
return appData?.auth || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findTriggersByKey(key, stripFuncs = false) {
|
||||||
|
const rawAppData = await getApp(key, stripFuncs);
|
||||||
|
const appData = appInfoConverter(rawAppData);
|
||||||
|
|
||||||
|
return appData?.triggers || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findTriggerSubsteps(appKey, triggerKey, stripFuncs = false) {
|
||||||
|
const rawAppData = await getApp(appKey, stripFuncs);
|
||||||
|
const appData = appInfoConverter(rawAppData);
|
||||||
|
|
||||||
|
const trigger = appData?.triggers?.find(
|
||||||
|
(trigger) => trigger.key === triggerKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return trigger?.substeps || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findActionsByKey(key, stripFuncs = false) {
|
||||||
|
const rawAppData = await getApp(key, stripFuncs);
|
||||||
|
const appData = appInfoConverter(rawAppData);
|
||||||
|
|
||||||
|
return appData?.actions || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static async findActionSubsteps(appKey, actionKey, stripFuncs = false) {
|
||||||
|
const rawAppData = await getApp(appKey, stripFuncs);
|
||||||
|
const appData = appInfoConverter(rawAppData);
|
||||||
|
|
||||||
|
const action = appData?.actions?.find((action) => action.key === actionKey);
|
||||||
|
|
||||||
|
return action?.substeps || [];
|
||||||
|
}
|
||||||
|
|
||||||
static async checkAppAndAction(appKey, actionKey) {
|
static async checkAppAndAction(appKey, actionKey) {
|
||||||
const app = await this.findOneByKey(appKey);
|
const app = await this.findOneByKey(appKey);
|
||||||
|
|
||||||
|
24
packages/backend/src/models/datastore.js
Normal file
24
packages/backend/src/models/datastore.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import Base from './base.js';
|
||||||
|
|
||||||
|
class Datastore extends Base {
|
||||||
|
static tableName = 'datastore';
|
||||||
|
|
||||||
|
static jsonSchema = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['key', 'value', 'scope', 'scopeId'],
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', format: 'uuid' },
|
||||||
|
key: { type: 'string', minLength: 1 },
|
||||||
|
value: { type: 'string' },
|
||||||
|
scope: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['flow'],
|
||||||
|
default: 'flow',
|
||||||
|
},
|
||||||
|
scopeId: { type: 'string', format: 'uuid' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Datastore;
|
@@ -149,6 +149,13 @@ class User extends Base {
|
|||||||
return conditions.isCreator ? this.$relatedQuery('flows') : Flow.query();
|
return conditions.isCreator ? this.$relatedQuery('flows') : Flow.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get authorizedExecutions() {
|
||||||
|
const conditions = this.can('read', 'Execution');
|
||||||
|
return conditions.isCreator
|
||||||
|
? this.$relatedQuery('executions')
|
||||||
|
: Execution.query();
|
||||||
|
}
|
||||||
|
|
||||||
login(password) {
|
login(password) {
|
||||||
return bcrypt.compare(password, this.password);
|
return bcrypt.compare(password, this.password);
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,41 @@ import { Router } from 'express';
|
|||||||
import asyncHandler from 'express-async-handler';
|
import asyncHandler from 'express-async-handler';
|
||||||
import { authenticateUser } from '../../../helpers/authentication.js';
|
import { authenticateUser } from '../../../helpers/authentication.js';
|
||||||
import getAppAction from '../../../controllers/api/v1/apps/get-app.js';
|
import getAppAction from '../../../controllers/api/v1/apps/get-app.js';
|
||||||
|
import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js';
|
||||||
|
import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js';
|
||||||
|
import getTriggersAction from '../../../controllers/api/v1/apps/get-triggers.js';
|
||||||
|
import getTriggerSubstepsAction from '../../../controllers/api/v1/apps/get-trigger-substeps.js';
|
||||||
|
import getActionsAction from '../../../controllers/api/v1/apps/get-actions.js';
|
||||||
|
import getActionSubstepsAction from '../../../controllers/api/v1/apps/get-action-substeps.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', authenticateUser, asyncHandler(getAppsAction));
|
||||||
router.get('/:appKey', authenticateUser, asyncHandler(getAppAction));
|
router.get('/:appKey', authenticateUser, asyncHandler(getAppAction));
|
||||||
|
router.get('/:appKey/auth', authenticateUser, asyncHandler(getAuthAction));
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:appKey/triggers',
|
||||||
|
authenticateUser,
|
||||||
|
asyncHandler(getTriggersAction)
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:appKey/triggers/:triggerKey/substeps',
|
||||||
|
authenticateUser,
|
||||||
|
asyncHandler(getTriggerSubstepsAction)
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:appKey/actions',
|
||||||
|
authenticateUser,
|
||||||
|
asyncHandler(getActionsAction)
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:appKey/actions/:actionKey/substeps',
|
||||||
|
authenticateUser,
|
||||||
|
asyncHandler(getActionSubstepsAction)
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
32
packages/backend/src/routes/api/v1/executions.js
Normal file
32
packages/backend/src/routes/api/v1/executions.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import asyncHandler from 'express-async-handler';
|
||||||
|
import { authenticateUser } from '../../../helpers/authentication.js';
|
||||||
|
import { authorizeUser } from '../../../helpers/authorization.js';
|
||||||
|
import getExecutionsAction from '../../../controllers/api/v1/executions/get-executions.js';
|
||||||
|
import getExecutionAction from '../../../controllers/api/v1/executions/get-execution.js';
|
||||||
|
import getExecutionStepsAction from '../../../controllers/api/v1/executions/get-execution-steps.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
authenticateUser,
|
||||||
|
authorizeUser,
|
||||||
|
asyncHandler(getExecutionsAction)
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:executionId',
|
||||||
|
authenticateUser,
|
||||||
|
authorizeUser,
|
||||||
|
asyncHandler(getExecutionAction)
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:executionId/execution-steps',
|
||||||
|
authenticateUser,
|
||||||
|
authorizeUser,
|
||||||
|
asyncHandler(getExecutionStepsAction)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
@@ -9,6 +9,7 @@ import paymentRouter from './api/v1/payment.ee.js';
|
|||||||
import appAuthClientsRouter from './api/v1/app-auth-clients.js';
|
import appAuthClientsRouter from './api/v1/app-auth-clients.js';
|
||||||
import flowsRouter from './api/v1/flows.js';
|
import flowsRouter from './api/v1/flows.js';
|
||||||
import appsRouter from './api/v1/apps.js';
|
import appsRouter from './api/v1/apps.js';
|
||||||
|
import executionsRouter from './api/v1/executions.js';
|
||||||
import samlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js';
|
import samlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js';
|
||||||
import rolesRouter from './api/v1/admin/roles.ee.js';
|
import rolesRouter from './api/v1/admin/roles.ee.js';
|
||||||
import permissionsRouter from './api/v1/admin/permissions.ee.js';
|
import permissionsRouter from './api/v1/admin/permissions.ee.js';
|
||||||
@@ -27,6 +28,7 @@ router.use('/api/v1/payment', paymentRouter);
|
|||||||
router.use('/api/v1/app-auth-clients', appAuthClientsRouter);
|
router.use('/api/v1/app-auth-clients', appAuthClientsRouter);
|
||||||
router.use('/api/v1/flows', flowsRouter);
|
router.use('/api/v1/flows', flowsRouter);
|
||||||
router.use('/api/v1/apps', appsRouter);
|
router.use('/api/v1/apps', appsRouter);
|
||||||
|
router.use('/api/v1/executions', executionsRouter);
|
||||||
router.use('/api/v1/admin/saml-auth-providers', samlAuthProvidersRouter);
|
router.use('/api/v1/admin/saml-auth-providers', samlAuthProvidersRouter);
|
||||||
router.use('/api/v1/admin/roles', rolesRouter);
|
router.use('/api/v1/admin/roles', rolesRouter);
|
||||||
router.use('/api/v1/admin/permissions', permissionsRouter);
|
router.use('/api/v1/admin/permissions', permissionsRouter);
|
||||||
|
9
packages/backend/src/serializers/action.js
Normal file
9
packages/backend/src/serializers/action.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const actionSerializer = (action) => {
|
||||||
|
return {
|
||||||
|
name: action.name,
|
||||||
|
key: action.key,
|
||||||
|
description: action.description,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default actionSerializer;
|
21
packages/backend/src/serializers/action.test.js
Normal file
21
packages/backend/src/serializers/action.test.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import App from '../models/app';
|
||||||
|
import actionSerializer from './action';
|
||||||
|
|
||||||
|
describe('actionSerializer', () => {
|
||||||
|
it('should return the action data', async () => {
|
||||||
|
const actions = await App.findActionsByKey('github');
|
||||||
|
const action = actions[0];
|
||||||
|
|
||||||
|
const expectedPayload = {
|
||||||
|
description: action.description,
|
||||||
|
key: action.key,
|
||||||
|
name: action.name,
|
||||||
|
pollInterval: action.pollInterval,
|
||||||
|
showWebhookUrl: action.showWebhookUrl,
|
||||||
|
type: action.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(actionSerializer(action)).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
@@ -3,7 +3,7 @@ import App from '../models/app';
|
|||||||
import appSerializer from './app';
|
import appSerializer from './app';
|
||||||
|
|
||||||
describe('appSerializer', () => {
|
describe('appSerializer', () => {
|
||||||
it('should return permission data', async () => {
|
it('should return app data', async () => {
|
||||||
const app = await App.findOneByKey('deepl');
|
const app = await App.findOneByKey('deepl');
|
||||||
|
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
|
9
packages/backend/src/serializers/auth.js
Normal file
9
packages/backend/src/serializers/auth.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const authSerializer = (auth) => {
|
||||||
|
return {
|
||||||
|
fields: auth.fields,
|
||||||
|
authenticationSteps: auth.authenticationSteps,
|
||||||
|
reconnectionSteps: auth.reconnectionSteps,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default authSerializer;
|
17
packages/backend/src/serializers/auth.test.js
Normal file
17
packages/backend/src/serializers/auth.test.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import App from '../models/app';
|
||||||
|
import authSerializer from './auth';
|
||||||
|
|
||||||
|
describe('authSerializer', () => {
|
||||||
|
it('should return auth data', async () => {
|
||||||
|
const auth = await App.findAuthByKey('deepl');
|
||||||
|
|
||||||
|
const expectedPayload = {
|
||||||
|
fields: auth.fields,
|
||||||
|
authenticationSteps: auth.authenticationSteps,
|
||||||
|
reconnectionSteps: auth.reconnectionSteps,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(authSerializer(auth)).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
21
packages/backend/src/serializers/execution-step.js
Normal file
21
packages/backend/src/serializers/execution-step.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import stepSerializer from './step.js';
|
||||||
|
|
||||||
|
const executionStepSerializer = (executionStep) => {
|
||||||
|
let executionStepData = {
|
||||||
|
id: executionStep.id,
|
||||||
|
dataIn: executionStep.dataIn,
|
||||||
|
dataOut: executionStep.dataOut,
|
||||||
|
errorDetails: executionStep.errorDetails,
|
||||||
|
status: executionStep.status,
|
||||||
|
createdAt: executionStep.createdAt.getTime(),
|
||||||
|
updatedAt: executionStep.updatedAt.getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (executionStep.step) {
|
||||||
|
executionStepData.step = stepSerializer(executionStep.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
return executionStepData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default executionStepSerializer;
|
43
packages/backend/src/serializers/execution-step.test.js
Normal file
43
packages/backend/src/serializers/execution-step.test.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import executionStepSerializer from './execution-step';
|
||||||
|
import stepSerializer from './step';
|
||||||
|
import { createExecutionStep } from '../../test/factories/execution-step';
|
||||||
|
import { createStep } from '../../test/factories/step';
|
||||||
|
|
||||||
|
describe('executionStepSerializer', () => {
|
||||||
|
let executionStep, step;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
step = await createStep();
|
||||||
|
|
||||||
|
executionStep = await createExecutionStep({
|
||||||
|
stepId: step.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the execution step data', async () => {
|
||||||
|
const expectedPayload = {
|
||||||
|
id: executionStep.id,
|
||||||
|
dataIn: executionStep.dataIn,
|
||||||
|
dataOut: executionStep.dataOut,
|
||||||
|
errorDetails: executionStep.errorDetails,
|
||||||
|
status: executionStep.status,
|
||||||
|
createdAt: executionStep.createdAt.getTime(),
|
||||||
|
updatedAt: executionStep.updatedAt.getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(executionStepSerializer(executionStep)).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the execution step data with the step', async () => {
|
||||||
|
executionStep.step = step;
|
||||||
|
|
||||||
|
const expectedPayload = {
|
||||||
|
step: stepSerializer(step),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(executionStepSerializer(executionStep)).toMatchObject(
|
||||||
|
expectedPayload
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
22
packages/backend/src/serializers/execution.js
Normal file
22
packages/backend/src/serializers/execution.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import flowSerializer from './flow.js';
|
||||||
|
|
||||||
|
const executionSerializer = (execution) => {
|
||||||
|
let executionData = {
|
||||||
|
id: execution.id,
|
||||||
|
testRun: execution.testRun,
|
||||||
|
createdAt: execution.createdAt.getTime(),
|
||||||
|
updatedAt: execution.updatedAt.getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (execution.status) {
|
||||||
|
executionData.status = execution.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (execution.flow) {
|
||||||
|
executionData.flow = flowSerializer(execution.flow);
|
||||||
|
}
|
||||||
|
|
||||||
|
return executionData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default executionSerializer;
|
52
packages/backend/src/serializers/execution.test.js
Normal file
52
packages/backend/src/serializers/execution.test.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import executionSerializer from './execution';
|
||||||
|
import flowSerializer from './flow';
|
||||||
|
import { createExecution } from '../../test/factories/execution';
|
||||||
|
import { createFlow } from '../../test/factories/flow';
|
||||||
|
|
||||||
|
describe('executionSerializer', () => {
|
||||||
|
let flow, execution;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
flow = await createFlow();
|
||||||
|
|
||||||
|
execution = await createExecution({
|
||||||
|
flowId: flow.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the execution data', async () => {
|
||||||
|
const expectedPayload = {
|
||||||
|
id: execution.id,
|
||||||
|
testRun: execution.testRun,
|
||||||
|
createdAt: execution.createdAt.getTime(),
|
||||||
|
updatedAt: execution.updatedAt.getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(executionSerializer(execution)).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the execution data with status', async () => {
|
||||||
|
execution.status = 'success';
|
||||||
|
|
||||||
|
const expectedPayload = {
|
||||||
|
id: execution.id,
|
||||||
|
testRun: execution.testRun,
|
||||||
|
createdAt: execution.createdAt.getTime(),
|
||||||
|
updatedAt: execution.updatedAt.getTime(),
|
||||||
|
status: 'success',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(executionSerializer(execution)).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the execution data with the flow', async () => {
|
||||||
|
execution.flow = flow;
|
||||||
|
|
||||||
|
const expectedPayload = {
|
||||||
|
flow: flowSerializer(flow),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(executionSerializer(execution)).toMatchObject(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
@@ -8,7 +8,7 @@ const flowSerializer = (flow) => {
|
|||||||
status: flow.status,
|
status: flow.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (flow.steps) {
|
if (flow.steps?.length > 0) {
|
||||||
flowData.steps = flow.steps.map((step) => stepSerializer(step));
|
flowData.steps = flow.steps.map((step) => stepSerializer(step));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,6 +6,11 @@ import appAuthClientSerializer from './app-auth-client.js';
|
|||||||
import flowSerializer from './flow.js';
|
import flowSerializer from './flow.js';
|
||||||
import stepSerializer from './step.js';
|
import stepSerializer from './step.js';
|
||||||
import appSerializer from './app.js';
|
import appSerializer from './app.js';
|
||||||
|
import authSerializer from './auth.js';
|
||||||
|
import triggerSerializer from './trigger.js';
|
||||||
|
import actionSerializer from './action.js';
|
||||||
|
import executionSerializer from './execution.js';
|
||||||
|
import executionStepSerializer from './execution-step.js';
|
||||||
|
|
||||||
const serializers = {
|
const serializers = {
|
||||||
User: userSerializer,
|
User: userSerializer,
|
||||||
@@ -16,6 +21,11 @@ const serializers = {
|
|||||||
Flow: flowSerializer,
|
Flow: flowSerializer,
|
||||||
Step: stepSerializer,
|
Step: stepSerializer,
|
||||||
App: appSerializer,
|
App: appSerializer,
|
||||||
|
Auth: authSerializer,
|
||||||
|
Trigger: triggerSerializer,
|
||||||
|
Action: actionSerializer,
|
||||||
|
Execution: executionSerializer,
|
||||||
|
ExecutionStep: executionStepSerializer,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default serializers;
|
export default serializers;
|
||||||
|
@@ -11,7 +11,7 @@ const roleSerializer = (role) => {
|
|||||||
isAdmin: role.isAdmin,
|
isAdmin: role.isAdmin,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (role.permissions) {
|
if (role.permissions?.length > 0) {
|
||||||
roleData.permissions = role.permissions.map((permission) =>
|
roleData.permissions = role.permissions.map((permission) =>
|
||||||
permissionSerializer(permission)
|
permissionSerializer(permission)
|
||||||
);
|
);
|
||||||
|
12
packages/backend/src/serializers/trigger.js
Normal file
12
packages/backend/src/serializers/trigger.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const triggerSerializer = (trigger) => {
|
||||||
|
return {
|
||||||
|
description: trigger.description,
|
||||||
|
key: trigger.key,
|
||||||
|
name: trigger.name,
|
||||||
|
pollInterval: trigger.pollInterval,
|
||||||
|
showWebhookUrl: trigger.showWebhookUrl,
|
||||||
|
type: trigger.type,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default triggerSerializer;
|
21
packages/backend/src/serializers/trigger.test.js
Normal file
21
packages/backend/src/serializers/trigger.test.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import App from '../models/app';
|
||||||
|
import triggerSerializer from './trigger';
|
||||||
|
|
||||||
|
describe('triggerSerializer', () => {
|
||||||
|
it('should return the trigger data', async () => {
|
||||||
|
const triggers = await App.findTriggersByKey('github');
|
||||||
|
const trigger = triggers[0];
|
||||||
|
|
||||||
|
const expectedPayload = {
|
||||||
|
description: trigger.description,
|
||||||
|
key: trigger.key,
|
||||||
|
name: trigger.name,
|
||||||
|
pollInterval: trigger.pollInterval,
|
||||||
|
showWebhookUrl: trigger.showWebhookUrl,
|
||||||
|
type: trigger.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(triggerSerializer(trigger)).toEqual(expectedPayload);
|
||||||
|
});
|
||||||
|
});
|
@@ -15,7 +15,7 @@ const userSerializer = (user) => {
|
|||||||
userData.role = roleSerializer(user.role);
|
userData.role = roleSerializer(user.role);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.permissions) {
|
if (user.permissions?.length > 0) {
|
||||||
userData.permissions = user.permissions.map((permission) =>
|
userData.permissions = user.permissions.map((permission) =>
|
||||||
permissionSerializer(permission)
|
permissionSerializer(permission)
|
||||||
);
|
);
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
const getActionSubstepsMock = (substeps) => {
|
||||||
|
return {
|
||||||
|
data: substeps,
|
||||||
|
meta: {
|
||||||
|
count: substeps.length,
|
||||||
|
currentPage: null,
|
||||||
|
isArray: true,
|
||||||
|
totalPages: null,
|
||||||
|
type: 'Object',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getActionSubstepsMock;
|
22
packages/backend/test/mocks/rest/api/v1/apps/get-actions.js
Normal file
22
packages/backend/test/mocks/rest/api/v1/apps/get-actions.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const getActionsMock = (actions) => {
|
||||||
|
const actionsData = actions.map((trigger) => {
|
||||||
|
return {
|
||||||
|
name: trigger.name,
|
||||||
|
key: trigger.key,
|
||||||
|
description: trigger.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: actionsData,
|
||||||
|
meta: {
|
||||||
|
count: actions.length,
|
||||||
|
currentPage: null,
|
||||||
|
isArray: true,
|
||||||
|
totalPages: null,
|
||||||
|
type: 'Object',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getActionsMock;
|
23
packages/backend/test/mocks/rest/api/v1/apps/get-apps.js
Normal file
23
packages/backend/test/mocks/rest/api/v1/apps/get-apps.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const getAppsMock = (apps) => {
|
||||||
|
const appsData = apps.map((app) => ({
|
||||||
|
authDocUrl: app.authDocUrl,
|
||||||
|
iconUrl: app.iconUrl,
|
||||||
|
key: app.key,
|
||||||
|
name: app.name,
|
||||||
|
primaryColor: app.primaryColor,
|
||||||
|
supportsConnections: app.supportsConnections,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: appsData,
|
||||||
|
meta: {
|
||||||
|
count: appsData.length,
|
||||||
|
currentPage: null,
|
||||||
|
isArray: true,
|
||||||
|
totalPages: null,
|
||||||
|
type: 'Object',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAppsMock;
|
18
packages/backend/test/mocks/rest/api/v1/apps/get-auth.js
Normal file
18
packages/backend/test/mocks/rest/api/v1/apps/get-auth.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const getAuthMock = (auth) => {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
fields: auth.fields,
|
||||||
|
authenticationSteps: auth.authenticationSteps,
|
||||||
|
reconnectionSteps: auth.reconnectionSteps,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
count: 1,
|
||||||
|
currentPage: null,
|
||||||
|
isArray: false,
|
||||||
|
totalPages: null,
|
||||||
|
type: 'Object',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAuthMock;
|
@@ -0,0 +1,14 @@
|
|||||||
|
const getTriggerSubstepsMock = (substeps) => {
|
||||||
|
return {
|
||||||
|
data: substeps,
|
||||||
|
meta: {
|
||||||
|
count: substeps.length,
|
||||||
|
currentPage: null,
|
||||||
|
isArray: true,
|
||||||
|
totalPages: null,
|
||||||
|
type: 'Object',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getTriggerSubstepsMock;
|
25
packages/backend/test/mocks/rest/api/v1/apps/get-triggers.js
Normal file
25
packages/backend/test/mocks/rest/api/v1/apps/get-triggers.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const getTriggersMock = (triggers) => {
|
||||||
|
const triggersData = triggers.map((trigger) => {
|
||||||
|
return {
|
||||||
|
description: trigger.description,
|
||||||
|
key: trigger.key,
|
||||||
|
name: trigger.name,
|
||||||
|
pollInterval: trigger.pollInterval,
|
||||||
|
showWebhookUrl: trigger.showWebhookUrl,
|
||||||
|
type: trigger.type,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: triggersData,
|
||||||
|
meta: {
|
||||||
|
count: triggers.length,
|
||||||
|
currentPage: null,
|
||||||
|
isArray: true,
|
||||||
|
totalPages: null,
|
||||||
|
type: 'Object',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getTriggersMock;
|
@@ -0,0 +1,39 @@
|
|||||||
|
const getExecutionStepsMock = async (executionSteps, steps) => {
|
||||||
|
const data = executionSteps.map((executionStep) => {
|
||||||
|
const step = steps.find((step) => step.id === executionStep.stepId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: executionStep.id,
|
||||||
|
dataIn: executionStep.dataIn,
|
||||||
|
dataOut: executionStep.dataOut,
|
||||||
|
errorDetails: executionStep.errorDetails,
|
||||||
|
status: executionStep.status,
|
||||||
|
createdAt: executionStep.createdAt.getTime(),
|
||||||
|
updatedAt: executionStep.updatedAt.getTime(),
|
||||||
|
step: {
|
||||||
|
id: step.id,
|
||||||
|
type: step.type,
|
||||||
|
key: step.key,
|
||||||
|
appKey: step.appKey,
|
||||||
|
iconUrl: step.iconUrl,
|
||||||
|
webhookUrl: step.webhookUrl,
|
||||||
|
status: step.status,
|
||||||
|
position: step.position,
|
||||||
|
parameters: step.parameters,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
meta: {
|
||||||
|
count: executionSteps.length,
|
||||||
|
currentPage: 1,
|
||||||
|
isArray: true,
|
||||||
|
totalPages: 1,
|
||||||
|
type: 'ExecutionStep',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getExecutionStepsMock;
|
@@ -0,0 +1,38 @@
|
|||||||
|
const getExecutionMock = async (execution, flow, steps) => {
|
||||||
|
const data = {
|
||||||
|
id: execution.id,
|
||||||
|
testRun: execution.testRun,
|
||||||
|
createdAt: execution.createdAt.getTime(),
|
||||||
|
updatedAt: execution.updatedAt.getTime(),
|
||||||
|
flow: {
|
||||||
|
id: flow.id,
|
||||||
|
name: flow.name,
|
||||||
|
active: flow.active,
|
||||||
|
status: flow.active ? 'published' : 'draft',
|
||||||
|
steps: steps.map((step) => ({
|
||||||
|
id: step.id,
|
||||||
|
type: step.type,
|
||||||
|
key: step.key,
|
||||||
|
appKey: step.appKey,
|
||||||
|
iconUrl: step.iconUrl,
|
||||||
|
webhookUrl: step.webhookUrl,
|
||||||
|
status: step.status,
|
||||||
|
position: step.position,
|
||||||
|
parameters: step.parameters,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
meta: {
|
||||||
|
count: 1,
|
||||||
|
currentPage: null,
|
||||||
|
isArray: false,
|
||||||
|
totalPages: null,
|
||||||
|
type: 'Execution',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getExecutionMock;
|
@@ -0,0 +1,39 @@
|
|||||||
|
const getExecutionsMock = async (executions, flow, steps) => {
|
||||||
|
const data = executions.map((execution) => ({
|
||||||
|
id: execution.id,
|
||||||
|
testRun: execution.testRun,
|
||||||
|
createdAt: execution.createdAt.getTime(),
|
||||||
|
updatedAt: execution.updatedAt.getTime(),
|
||||||
|
status: 'success',
|
||||||
|
flow: {
|
||||||
|
id: flow.id,
|
||||||
|
name: flow.name,
|
||||||
|
active: flow.active,
|
||||||
|
status: flow.active ? 'published' : 'draft',
|
||||||
|
steps: steps.map((step) => ({
|
||||||
|
id: step.id,
|
||||||
|
type: step.type,
|
||||||
|
key: step.key,
|
||||||
|
appKey: step.appKey,
|
||||||
|
iconUrl: step.iconUrl,
|
||||||
|
webhookUrl: step.webhookUrl,
|
||||||
|
status: step.status,
|
||||||
|
position: step.position,
|
||||||
|
parameters: step.parameters,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
meta: {
|
||||||
|
count: executions.length,
|
||||||
|
currentPage: 1,
|
||||||
|
isArray: true,
|
||||||
|
totalPages: 1,
|
||||||
|
type: 'Execution',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getExecutionsMock;
|
@@ -1,11 +1,19 @@
|
|||||||
const getCurrentUserMock = (currentUser, role) => {
|
const getCurrentUserMock = (currentUser, role, permissions) => {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
createdAt: currentUser.createdAt.getTime(),
|
createdAt: currentUser.createdAt.getTime(),
|
||||||
email: currentUser.email,
|
email: currentUser.email,
|
||||||
fullName: currentUser.fullName,
|
fullName: currentUser.fullName,
|
||||||
id: currentUser.id,
|
id: currentUser.id,
|
||||||
permissions: [],
|
permissions: permissions.map((permission) => ({
|
||||||
|
id: permission.id,
|
||||||
|
roleId: permission.roleId,
|
||||||
|
action: permission.action,
|
||||||
|
subject: permission.subject,
|
||||||
|
conditions: permission.conditions,
|
||||||
|
createdAt: permission.createdAt.getTime(),
|
||||||
|
updatedAt: permission.updatedAt.getTime(),
|
||||||
|
})),
|
||||||
role: {
|
role: {
|
||||||
createdAt: role.createdAt.getTime(),
|
createdAt: role.createdAt.getTime(),
|
||||||
description: null,
|
description: null,
|
||||||
|
@@ -41,6 +41,15 @@ export default defineConfig({
|
|||||||
{ text: 'Connection', link: '/apps/carbone/connection' },
|
{ text: 'Connection', link: '/apps/carbone/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Datastore',
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Actions', link: '/apps/datastore/actions' },
|
||||||
|
{ text: 'Connection', link: '/apps/datastore/connection' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'DeepL',
|
text: 'DeepL',
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
@@ -305,7 +314,7 @@ export default defineConfig({
|
|||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'Actions', link: '/apps/removebg/actions' },
|
{ text: 'Actions', link: '/apps/removebg/actions' },
|
||||||
{ text: 'Connection', link: '/apps/removebg/connection' }
|
{ text: 'Connection', link: '/apps/removebg/connection' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
14
packages/docs/pages/apps/datastore/actions.md
Normal file
14
packages/docs/pages/apps/datastore/actions.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
favicon: /favicons/datastore.svg
|
||||||
|
items:
|
||||||
|
- name: Get value
|
||||||
|
desc: Get value from the persistent datastore.
|
||||||
|
- name: Set value
|
||||||
|
desc: Set value to the persistent datastore.
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CustomListing from '../../components/CustomListing.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CustomListing />
|
3
packages/docs/pages/apps/datastore/connection.md
Normal file
3
packages/docs/pages/apps/datastore/connection.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Datastore
|
||||||
|
|
||||||
|
Datastore is a persistent key-value storage system that allows you to store and retrieve data. Currently you can use it within the scope of the flow, meaning you can store and retrieve data within the same flow.
|
@@ -3,6 +3,7 @@
|
|||||||
The following integrations are currently supported by Automatisch.
|
The following integrations are currently supported by Automatisch.
|
||||||
|
|
||||||
- [Carbone](/apps/carbone/actions)
|
- [Carbone](/apps/carbone/actions)
|
||||||
|
- [Datastore](/apps/datastore/actions)
|
||||||
- [DeepL](/apps/deepl/actions)
|
- [DeepL](/apps/deepl/actions)
|
||||||
- [Delay](/apps/delay/actions)
|
- [Delay](/apps/delay/actions)
|
||||||
- [Discord](/apps/discord/actions)
|
- [Discord](/apps/discord/actions)
|
||||||
|
13
packages/docs/pages/public/favicons/datastore.svg
Normal file
13
packages/docs/pages/public/favicons/datastore.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" id="icon">
|
||||||
|
<defs>
|
||||||
|
<style>.cls-1{fill:none;}</style>
|
||||||
|
</defs>
|
||||||
|
<title>datastore</title>
|
||||||
|
<circle cx="23" cy="23" r="1"/>
|
||||||
|
<rect x="8" y="22" width="12" height="2"/>
|
||||||
|
<circle cx="23" cy="9" r="1"/>
|
||||||
|
<rect x="8" y="8" width="12" height="2"/>
|
||||||
|
<path d="M26,14a2,2,0,0,0,2-2V6a2,2,0,0,0-2-2H6A2,2,0,0,0,4,6v6a2,2,0,0,0,2,2H8v4H6a2,2,0,0,0-2,2v6a2,2,0,0,0,2,2H26a2,2,0,0,0,2-2V20a2,2,0,0,0-2-2H24V14ZM6,6H26v6H6ZM26,26H6V20H26Zm-4-8H10V14H22Z"/>
|
||||||
|
<rect id="_Transparent_Rectangle_" data-name="<Transparent Rectangle>" class="cls-1" width="32" height="32"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 704 B |
@@ -31,7 +31,7 @@ export class AdminRolesPage extends AuthenticatedPage {
|
|||||||
await this.roleDrawerLink.click();
|
await this.roleDrawerLink.click();
|
||||||
await this.isMounted();
|
await this.isMounted();
|
||||||
await this.rolesLoader.waitFor({
|
await this.rolesLoader.waitFor({
|
||||||
state: 'detached'
|
state: 'detached',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,9 +43,7 @@ export class AdminRolesPage extends AuthenticatedPage {
|
|||||||
state: 'detached',
|
state: 'detached',
|
||||||
});
|
});
|
||||||
return this.roleRow.filter({
|
return this.roleRow.filter({
|
||||||
has: this.page.getByTestId('role-name').filter({
|
has: this.page.getByTestId('role-name').getByText(name, { exact: true }),
|
||||||
hasText: name,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -28,8 +28,6 @@
|
|||||||
"@playwright/test": "^1.36.2"
|
"@playwright/test": "^1.36.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
|
||||||
"@typescript-eslint/parser": "^5.9.1",
|
|
||||||
"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",
|
||||||
|
@@ -197,7 +197,7 @@ test.describe('Role management page', () => {
|
|||||||
await adminCreateUserPage.passwordInput.fill('sample');
|
await adminCreateUserPage.passwordInput.fill('sample');
|
||||||
await adminCreateUserPage.roleInput.click();
|
await adminCreateUserPage.roleInput.click();
|
||||||
await adminCreateUserPage.page
|
await adminCreateUserPage.page
|
||||||
.getByRole('option', { name: 'Delete Role' })
|
.getByRole('option', { name: 'Delete Role', exact: true })
|
||||||
.click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
await adminUsersPage.snackbar.waitFor({
|
await adminUsersPage.snackbar.waitFor({
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
PORT=3001
|
PORT=3001
|
||||||
REACT_APP_GRAPHQL_URL=http://localhost:3000/graphql
|
REACT_APP_BACKEND_URL=http://localhost:3000
|
||||||
# HTTPS=true
|
# HTTPS=true
|
||||||
REACT_APP_BASE_URL=http://localhost:3001
|
REACT_APP_BASE_URL=http://localhost:3001
|
||||||
|
4
packages/web/.eslintignore
Normal file
4
packages/web/.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
||||||
|
source
|
||||||
|
.eslintrc.js
|
10
packages/web/.eslintrc.js
Normal file
10
packages/web/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'react-app',
|
||||||
|
'plugin:@tanstack/eslint-plugin-query/recommended',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'react/prop-types': 'warn',
|
||||||
|
},
|
||||||
|
};
|
6
packages/web/jsconfig.json
Normal file
6
packages/web/jsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
@@ -7,23 +7,17 @@
|
|||||||
"@apollo/client": "^3.6.9",
|
"@apollo/client": "^3.6.9",
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@casl/react": "^3.1.0",
|
"@casl/react": "^3.1.0",
|
||||||
|
"@emotion/css": "^11.11.2",
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@hookform/resolvers": "^2.8.8",
|
"@hookform/resolvers": "^2.8.8",
|
||||||
"@mui/icons-material": "^5.11.9",
|
"@mui/icons-material": "^5.11.9",
|
||||||
"@mui/lab": "^5.0.0-alpha.120",
|
"@mui/lab": "^5.0.0-alpha.120",
|
||||||
"@mui/material": "^5.11.10",
|
"@mui/material": "^5.11.10",
|
||||||
|
"@tanstack/react-query": "^5.24.1",
|
||||||
"@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",
|
||||||
"@types/jest": "^26.0.15",
|
|
||||||
"@types/lodash": "^4.14.182",
|
|
||||||
"@types/luxon": "^2.0.8",
|
|
||||||
"@types/node": "^12.0.0",
|
|
||||||
"@types/react": "^17.0.0",
|
|
||||||
"@types/react-dom": "^17.0.0",
|
|
||||||
"@types/react-window": "^1.8.5",
|
|
||||||
"@types/uuid": "^9.0.0",
|
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"compare-versions": "^4.1.3",
|
"compare-versions": "^4.1.3",
|
||||||
"graphql": "^15.6.0",
|
"graphql": "^15.6.0",
|
||||||
@@ -31,8 +25,8 @@
|
|||||||
"luxon": "^2.3.1",
|
"luxon": "^2.3.1",
|
||||||
"mui-color-input": "^2.0.0",
|
"mui-color-input": "^2.0.0",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
"react": "^17.0.2",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.45.2",
|
"react-hook-form": "^7.45.2",
|
||||||
"react-intl": "^5.20.12",
|
"react-intl": "^5.20.12",
|
||||||
"react-json-tree": "^0.16.2",
|
"react-json-tree": "^0.16.2",
|
||||||
@@ -42,7 +36,6 @@
|
|||||||
"slate": "^0.94.1",
|
"slate": "^0.94.1",
|
||||||
"slate-history": "^0.93.0",
|
"slate-history": "^0.93.0",
|
||||||
"slate-react": "^0.94.2",
|
"slate-react": "^0.94.2",
|
||||||
"typescript": "^4.6.3",
|
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"web-vitals": "^1.0.1",
|
"web-vitals": "^1.0.1",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
@@ -54,8 +47,8 @@
|
|||||||
"build:watch": "yarn nodemon --exec react-scripts build --watch 'src/**/*.ts' --watch 'public/**/*' --ext ts,html",
|
"build:watch": "yarn nodemon --exec react-scripts build --watch 'src/**/*.ts' --watch 'public/**/*' --ext ts,html",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"lint": "eslint . --ignore-path ../../.eslintignore",
|
"lint": "eslint src --ext .js,.jsx",
|
||||||
"prepack": "REACT_APP_GRAPHQL_URL=/graphql yarn build"
|
"prepack": "yarn build"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"/build"
|
"/build"
|
||||||
@@ -87,5 +80,17 @@
|
|||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tanstack/eslint-plugin-query": "^5.20.1",
|
||||||
|
"@tanstack/react-query-devtools": "^5.24.1",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"prettier": "^3.2.5"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"./.eslintrc.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Route, Navigate } from 'react-router-dom';
|
import { Route, Navigate } from 'react-router-dom';
|
||||||
import AdminSettingsLayout from 'components/AdminSettingsLayout';
|
|
||||||
import Users from 'pages/Users';
|
import Users from 'pages/Users';
|
||||||
import EditUser from 'pages/EditUser';
|
import EditUser from 'pages/EditUser';
|
||||||
import CreateUser from 'pages/CreateUser';
|
import CreateUser from 'pages/CreateUser';
|
||||||
@@ -8,12 +8,10 @@ import CreateRole from 'pages/CreateRole/index.ee';
|
|||||||
import EditRole from 'pages/EditRole/index.ee';
|
import EditRole from 'pages/EditRole/index.ee';
|
||||||
import Authentication from 'pages/Authentication';
|
import Authentication from 'pages/Authentication';
|
||||||
import UserInterface from 'pages/UserInterface';
|
import UserInterface from 'pages/UserInterface';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
import AdminApplications from 'pages/AdminApplications';
|
import AdminApplications from 'pages/AdminApplications';
|
||||||
import AdminApplication from 'pages/AdminApplication';
|
import AdminApplication from 'pages/AdminApplication';
|
||||||
|
|
||||||
// TODO: consider introducing redirections to `/` as fallback
|
// TODO: consider introducing redirections to `/` as fallback
|
||||||
export default (
|
export default (
|
||||||
<>
|
<>
|
||||||
@@ -21,9 +19,7 @@ export default (
|
|||||||
path={URLS.USERS}
|
path={URLS.USERS}
|
||||||
element={
|
element={
|
||||||
<Can I="read" a="User">
|
<Can I="read" a="User">
|
||||||
<AdminSettingsLayout>
|
<Users />
|
||||||
<Users />
|
|
||||||
</AdminSettingsLayout>
|
|
||||||
</Can>
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -32,9 +28,7 @@ export default (
|
|||||||
path={URLS.CREATE_USER}
|
path={URLS.CREATE_USER}
|
||||||
element={
|
element={
|
||||||
<Can I="create" a="User">
|
<Can I="create" a="User">
|
||||||
<AdminSettingsLayout>
|
<CreateUser />
|
||||||
<CreateUser />
|
|
||||||
</AdminSettingsLayout>
|
|
||||||
</Can>
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -43,9 +37,7 @@ export default (
|
|||||||
path={URLS.USER_PATTERN}
|
path={URLS.USER_PATTERN}
|
||||||
element={
|
element={
|
||||||
<Can I="update" a="User">
|
<Can I="update" a="User">
|
||||||
<AdminSettingsLayout>
|
<EditUser />
|
||||||
<EditUser />
|
|
||||||
</AdminSettingsLayout>
|
|
||||||
</Can>
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -54,9 +46,7 @@ export default (
|
|||||||
path={URLS.ROLES}
|
path={URLS.ROLES}
|
||||||
element={
|
element={
|
||||||
<Can I="read" a="Role">
|
<Can I="read" a="Role">
|
||||||
<AdminSettingsLayout>
|
<Roles />
|
||||||
<Roles />
|
|
||||||
</AdminSettingsLayout>
|
|
||||||
</Can>
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -65,9 +55,7 @@ export default (
|
|||||||
path={URLS.CREATE_ROLE}
|
path={URLS.CREATE_ROLE}
|
||||||
element={
|
element={
|
||||||
<Can I="create" a="Role">
|
<Can I="create" a="Role">
|
||||||
<AdminSettingsLayout>
|
<CreateRole />
|
||||||
<CreateRole />
|
|
||||||
</AdminSettingsLayout>
|
|
||||||
</Can>
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -76,9 +64,7 @@ export default (
|
|||||||
path={URLS.ROLE_PATTERN}
|
path={URLS.ROLE_PATTERN}
|
||||||
element={
|
element={
|
||||||
<Can I="update" a="Role">
|
<Can I="update" a="Role">
|
||||||
<AdminSettingsLayout>
|
<EditRole />
|
||||||
<EditRole />
|
|
||||||
</AdminSettingsLayout>
|
|
||||||
</Can>
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -87,9 +73,7 @@ export default (
|
|||||||
path={URLS.USER_INTERFACE}
|
path={URLS.USER_INTERFACE}
|
||||||
element={
|
element={
|
||||||
<Can I="update" a="Config">
|
<Can I="update" a="Config">
|
||||||
<AdminSettingsLayout>
|
<UserInterface />
|
||||||
<UserInterface />
|
|
||||||
</AdminSettingsLayout>
|
|
||||||
</Can>
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -100,9 +84,7 @@ export default (
|
|||||||
<Can I="read" a="SamlAuthProvider">
|
<Can I="read" a="SamlAuthProvider">
|
||||||
<Can I="update" a="SamlAuthProvider">
|
<Can I="update" a="SamlAuthProvider">
|
||||||
<Can I="create" a="SamlAuthProvider">
|
<Can I="create" a="SamlAuthProvider">
|
||||||
<AdminSettingsLayout>
|
<Authentication />
|
||||||
<Authentication />
|
|
||||||
</AdminSettingsLayout>
|
|
||||||
</Can>
|
</Can>
|
||||||
</Can>
|
</Can>
|
||||||
</Can>
|
</Can>
|
||||||
@@ -113,9 +95,7 @@ export default (
|
|||||||
path={URLS.ADMIN_APPS}
|
path={URLS.ADMIN_APPS}
|
||||||
element={
|
element={
|
||||||
<Can I="update" a="App">
|
<Can I="update" a="App">
|
||||||
<AdminSettingsLayout>
|
<AdminApplications />
|
||||||
<AdminApplications />
|
|
||||||
</AdminSettingsLayout>
|
|
||||||
</Can>
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -124,9 +104,7 @@ export default (
|
|||||||
path={`${URLS.ADMIN_APP_PATTERN}/*`}
|
path={`${URLS.ADMIN_APP_PATTERN}/*`}
|
||||||
element={
|
element={
|
||||||
<Can I="update" a="App">
|
<Can I="update" a="App">
|
||||||
<AdminSettingsLayout>
|
<AdminApplication />
|
||||||
<AdminApplication />
|
|
||||||
</AdminSettingsLayout>
|
|
||||||
</Can>
|
</Can>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
@@ -1,40 +1,25 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import Menu, { MenuProps } from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
import apolloClient from 'graphql/client';
|
import apolloClient from 'graphql/client';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
function AccountDropdownMenu(props) {
|
||||||
type AccountDropdownMenuProps = {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
anchorEl: MenuProps['anchorEl'];
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function AccountDropdownMenu(
|
|
||||||
props: AccountDropdownMenuProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { open, onClose, anchorEl, id } = props;
|
const { open, onClose, anchorEl, id } = props;
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
authentication.updateToken('');
|
authentication.updateToken('');
|
||||||
await apolloClient.clearStore();
|
await apolloClient.clearStore();
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
navigate(URLS.LOGIN);
|
navigate(URLS.LOGIN);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
@@ -51,7 +36,7 @@ function AccountDropdownMenu(
|
|||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<MenuItem component={Link} to={URLS.SETTINGS_DASHBOARD}>
|
<MenuItem component={Link} to={URLS.SETTINGS_DASHBOARD} onClick={onClose}>
|
||||||
{formatMessage('accountDropdownMenu.settings')}
|
{formatMessage('accountDropdownMenu.settings')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
@@ -59,6 +44,7 @@ function AccountDropdownMenu(
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
component={Link}
|
component={Link}
|
||||||
to={URLS.ADMIN_SETTINGS_DASHBOARD}
|
to={URLS.ADMIN_SETTINGS_DASHBOARD}
|
||||||
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
{formatMessage('accountDropdownMenu.adminSettings')}
|
{formatMessage('accountDropdownMenu.adminSettings')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -71,4 +57,11 @@ function AccountDropdownMenu(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccountDropdownMenu.propTypes = {
|
||||||
|
open: PropTypes.bool.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default AccountDropdownMenu;
|
export default AccountDropdownMenu;
|
@@ -1,4 +1,4 @@
|
|||||||
import type { IApp, IField, IJSONObject } from 'types';
|
import PropTypes from 'prop-types';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
@@ -6,32 +6,24 @@ import DialogContent from '@mui/material/DialogContent';
|
|||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AppPropType } from 'propTypes/propTypes';
|
||||||
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||||
import InputCreator from 'components/InputCreator';
|
import InputCreator from 'components/InputCreator';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import { generateExternalLink } from '../../helpers/translationValues';
|
import { generateExternalLink } from 'helpers/translationValues';
|
||||||
import { Form } from './style';
|
import { Form } from './style';
|
||||||
|
|
||||||
type AddAppConnectionProps = {
|
function AddAppConnection(props) {
|
||||||
onClose: (response: Record<string, unknown>) => void;
|
|
||||||
application: IApp;
|
|
||||||
connectionId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AddAppConnection(
|
|
||||||
props: AddAppConnectionProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { application, connectionId, onClose } = props;
|
const { application, connectionId, onClose } = props;
|
||||||
const { name, authDocUrl, key, auth } = application;
|
const { name, authDocUrl, key, auth } = application;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [error, setError] = React.useState<IJSONObject | null>(null);
|
const [error, setError] = React.useState(null);
|
||||||
const [inProgress, setInProgress] = React.useState(false);
|
const [inProgress, setInProgress] = React.useState(false);
|
||||||
const hasConnection = Boolean(connectionId);
|
const hasConnection = Boolean(connectionId);
|
||||||
const useShared = searchParams.get('shared') === 'true';
|
const useShared = searchParams.get('shared') === 'true';
|
||||||
@@ -42,7 +34,6 @@ export default function AddAppConnection(
|
|||||||
appAuthClientId,
|
appAuthClientId,
|
||||||
useShared: !!appAuthClientId,
|
useShared: !!appAuthClientId,
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(function relayProviderData() {
|
React.useEffect(function relayProviderData() {
|
||||||
if (window.opener) {
|
if (window.opener) {
|
||||||
window.opener.postMessage({
|
window.opener.postMessage({
|
||||||
@@ -52,51 +43,41 @@ export default function AddAppConnection(
|
|||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function initiateSharedAuthenticationForGivenAuthClient() {
|
function initiateSharedAuthenticationForGivenAuthClient() {
|
||||||
if (!appAuthClientId) return;
|
if (!appAuthClientId) return;
|
||||||
if (!authenticate) return;
|
if (!authenticate) return;
|
||||||
|
|
||||||
const asyncAuthenticate = async () => {
|
const asyncAuthenticate = async () => {
|
||||||
await authenticate();
|
await authenticate();
|
||||||
|
|
||||||
navigate(URLS.APP_CONNECTIONS(key));
|
navigate(URLS.APP_CONNECTIONS(key));
|
||||||
};
|
};
|
||||||
|
|
||||||
asyncAuthenticate();
|
asyncAuthenticate();
|
||||||
},
|
},
|
||||||
[appAuthClientId, authenticate]
|
[appAuthClientId, authenticate],
|
||||||
);
|
);
|
||||||
|
const handleClientClick = (appAuthClientId) =>
|
||||||
const handleClientClick = (appAuthClientId: string) =>
|
|
||||||
navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId));
|
navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId));
|
||||||
|
|
||||||
const handleAuthClientsDialogClose = () =>
|
const handleAuthClientsDialogClose = () =>
|
||||||
navigate(URLS.APP_CONNECTIONS(key));
|
navigate(URLS.APP_CONNECTIONS(key));
|
||||||
|
const submitHandler = React.useCallback(
|
||||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
|
||||||
async (data) => {
|
async (data) => {
|
||||||
if (!authenticate) return;
|
if (!authenticate) return;
|
||||||
|
|
||||||
setInProgress(true);
|
setInProgress(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authenticate({
|
const response = await authenticate({
|
||||||
fields: data,
|
fields: data,
|
||||||
});
|
});
|
||||||
onClose(response as Record<string, unknown>);
|
onClose(response);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as IJSONObject;
|
const error = err;
|
||||||
console.log(error);
|
console.log(error);
|
||||||
setError((error.graphQLErrors as IJSONObject[])?.[0]);
|
setError(error.graphQLErrors?.[0]);
|
||||||
} finally {
|
} finally {
|
||||||
setInProgress(false);
|
setInProgress(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[authenticate]
|
[authenticate],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (useShared)
|
if (useShared)
|
||||||
return (
|
return (
|
||||||
<AppAuthClientsDialog
|
<AppAuthClientsDialog
|
||||||
@@ -105,9 +86,7 @@ export default function AddAppConnection(
|
|||||||
onClientClick={handleClientClick}
|
onClientClick={handleClientClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (appAuthClientId) return <React.Fragment />;
|
if (appAuthClientId) return <React.Fragment />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
|
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
@@ -142,7 +121,7 @@ export default function AddAppConnection(
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText tabIndex={-1} component="div">
|
<DialogContentText tabIndex={-1} component="div">
|
||||||
<Form onSubmit={submitHandler}>
|
<Form onSubmit={submitHandler}>
|
||||||
{auth?.fields?.map((field: IField) => (
|
{auth?.fields?.map((field) => (
|
||||||
<InputCreator key={field.key} schema={field} />
|
<InputCreator key={field.key} schema={field} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -162,3 +141,11 @@ export default function AddAppConnection(
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AddAppConnection.propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
application: AppPropType.isRequired,
|
||||||
|
connectionId: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddAppConnection;
|
@@ -1,6 +1,5 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import BaseForm from 'components/Form';
|
import BaseForm from 'components/Form';
|
||||||
|
|
||||||
export const Form = styled(BaseForm)(({ theme }) => ({
|
export const Form = styled(BaseForm)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useLazyQuery } from '@apollo/client';
|
import { useLazyQuery } from '@apollo/client';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
@@ -19,67 +20,53 @@ import InputLabel from '@mui/material/InputLabel';
|
|||||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||||
import FormControl from '@mui/material/FormControl';
|
import FormControl from '@mui/material/FormControl';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import type { IApp } from 'types';
|
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import AppIcon from 'components/AppIcon';
|
import AppIcon from 'components/AppIcon';
|
||||||
import { GET_APPS } from 'graphql/queries/get-apps';
|
import { GET_APPS } from 'graphql/queries/get-apps';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
function createConnectionOrFlow(appKey: string, supportsConnections = false) {
|
function createConnectionOrFlow(appKey, supportsConnections = false) {
|
||||||
if (!supportsConnections) {
|
if (!supportsConnections) {
|
||||||
return URLS.CREATE_FLOW_WITH_APP(appKey);
|
return URLS.CREATE_FLOW_WITH_APP(appKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return URLS.APP_ADD_CONNECTION(appKey);
|
return URLS.APP_ADD_CONNECTION(appKey);
|
||||||
}
|
}
|
||||||
|
function AddNewAppConnection(props) {
|
||||||
type AddNewAppConnectionProps = {
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AddNewAppConnection(
|
|
||||||
props: AddNewAppConnectionProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { onClose } = props;
|
const { onClose } = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('sm'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [appName, setAppName] = React.useState<string | null>(null);
|
const [appName, setAppName] = React.useState(null);
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
const [getApps, { data }] = useLazyQuery(GET_APPS, {
|
const [getApps, { data }] = useLazyQuery(GET_APPS, {
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchData = React.useMemo(
|
const fetchData = React.useMemo(
|
||||||
() => debounce((name) => getApps({ variables: { name } }), 300),
|
() => debounce((name) => getApps({ variables: { name } }), 300),
|
||||||
[getApps]
|
[getApps],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function fetchAppsOnAppNameChange() {
|
function fetchAppsOnAppNameChange() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
fetchData(appName);
|
fetchData(appName);
|
||||||
},
|
},
|
||||||
[fetchData, appName]
|
[fetchData, appName],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(function cancelDebounceOnUnmount() {
|
React.useEffect(function cancelDebounceOnUnmount() {
|
||||||
return () => {
|
return () => {
|
||||||
fetchData.cancel();
|
fetchData.cancel();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={true}
|
open={true}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
maxWidth="sm"
|
maxWidth="sm"
|
||||||
fullWidth
|
fullWidth
|
||||||
data-test="add-app-connection-dialog">
|
data-test="add-app-connection-dialog"
|
||||||
|
>
|
||||||
<DialogTitle>{formatMessage('apps.addNewAppConnection')}</DialogTitle>
|
<DialogTitle>{formatMessage('apps.addNewAppConnection')}</DialogTitle>
|
||||||
|
|
||||||
<Box px={3}>
|
<Box px={3}>
|
||||||
@@ -123,7 +110,7 @@ export default function AddNewAppConnection(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading &&
|
{!loading &&
|
||||||
data?.getApps?.map((app: IApp) => (
|
data?.getApps?.map((app) => (
|
||||||
<ListItem disablePadding key={app.name} data-test="app-list-item">
|
<ListItem disablePadding key={app.name} data-test="app-list-item">
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
component={Link}
|
component={Link}
|
||||||
@@ -151,3 +138,9 @@ export default function AddNewAppConnection(
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AddNewAppConnection.propTypes = {
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddNewAppConnection;
|
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import * as React from 'react';
|
||||||
import type { IField } from 'types';
|
import PropTypes from 'prop-types';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
@@ -7,32 +7,16 @@ import DialogContent from '@mui/material/DialogContent';
|
|||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
import { ApolloError } from '@apollo/client';
|
||||||
import type { UseFormProps } from 'react-hook-form';
|
|
||||||
import type { ApolloError } from '@apollo/client';
|
|
||||||
|
|
||||||
|
import { FieldPropType } from 'propTypes/propTypes';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import InputCreator from 'components/InputCreator';
|
import InputCreator from 'components/InputCreator';
|
||||||
import Switch from 'components/Switch';
|
import Switch from 'components/Switch';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
|
|
||||||
import { Form } from './style';
|
import { Form } from './style';
|
||||||
|
|
||||||
type AdminApplicationAuthClientDialogProps = {
|
function AdminApplicationAuthClientDialog(props) {
|
||||||
title: string;
|
|
||||||
authFields?: IField[];
|
|
||||||
defaultValues: UseFormProps['defaultValues'];
|
|
||||||
loading: boolean;
|
|
||||||
submitting: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
error?: ApolloError;
|
|
||||||
submitHandler: SubmitHandler<FieldValues>;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminApplicationAuthClientDialog(
|
|
||||||
props: AdminApplicationAuthClientDialogProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -45,7 +29,6 @@ export default function AdminApplicationAuthClientDialog(
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
} = props;
|
} = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onClose={onClose}>
|
<Dialog open={true} onClose={onClose}>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
@@ -80,7 +63,7 @@ export default function AdminApplicationAuthClientDialog(
|
|||||||
label={formatMessage('authClient.inputName')}
|
label={formatMessage('authClient.inputName')}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
{authFields?.map((field: IField) => (
|
{authFields?.map((field) => (
|
||||||
<InputCreator key={field.key} schema={field} />
|
<InputCreator key={field.key} schema={field} />
|
||||||
))}
|
))}
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
@@ -102,3 +85,17 @@ export default function AdminApplicationAuthClientDialog(
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AdminApplicationAuthClientDialog.propTypes = {
|
||||||
|
error: PropTypes.instanceOf(ApolloError),
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
submitHandler: PropTypes.func.isRequired,
|
||||||
|
authFields: PropTypes.arrayOf(FieldPropType),
|
||||||
|
submitting: PropTypes.bool.isRequired,
|
||||||
|
defaultValues: PropTypes.object.isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminApplicationAuthClientDialog;
|
@@ -1,6 +1,5 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import BaseForm from 'components/Form';
|
import BaseForm from 'components/Form';
|
||||||
|
|
||||||
export const Form = styled(BaseForm)(({ theme }) => ({
|
export const Form = styled(BaseForm)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
@@ -1,3 +1,4 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
@@ -7,27 +8,17 @@ import CardContent from '@mui/material/CardContent';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
||||||
|
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
|
|
||||||
type AdminApplicationAuthClientsProps = {
|
function AdminApplicationAuthClients(props) {
|
||||||
appKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function AdminApplicationAuthClients(
|
|
||||||
props: AdminApplicationAuthClientsProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { appKey } = props;
|
const { appKey } = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { appAuthClients, loading } = useAppAuthClients({ appKey });
|
const { appAuthClients, loading } = useAppAuthClients({ appKey });
|
||||||
|
|
||||||
if (loading)
|
if (loading)
|
||||||
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
|
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
|
||||||
|
|
||||||
if (!appAuthClients?.length) {
|
if (!appAuthClients?.length) {
|
||||||
return (
|
return (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
@@ -36,7 +27,6 @@ function AdminApplicationAuthClients(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedAuthClients = appAuthClients.slice().sort((a, b) => {
|
const sortedAuthClients = appAuthClients.slice().sort((a, b) => {
|
||||||
if (a.id < b.id) {
|
if (a.id < b.id) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -46,7 +36,6 @@ function AdminApplicationAuthClients(
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{sortedAuthClients.map((client) => (
|
{sortedAuthClients.map((client) => (
|
||||||
@@ -67,7 +56,7 @@ function AdminApplicationAuthClients(
|
|||||||
label={formatMessage(
|
label={formatMessage(
|
||||||
client?.active
|
client?.active
|
||||||
? 'adminAppsAuthClients.statusActive'
|
? 'adminAppsAuthClients.statusActive'
|
||||||
: 'adminAppsAuthClients.statusInactive'
|
: 'adminAppsAuthClients.statusInactive',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -86,4 +75,8 @@ function AdminApplicationAuthClients(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AdminApplicationAuthClients.propTypes = {
|
||||||
|
appKey: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default AdminApplicationAuthClients;
|
export default AdminApplicationAuthClients;
|
@@ -1,24 +1,15 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import type { IApp } from 'types';
|
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
|
|
||||||
|
import { AppPropType } from 'propTypes/propTypes';
|
||||||
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
||||||
import { CREATE_APP_AUTH_CLIENT } from 'graphql/mutations/create-app-auth-client';
|
import { CREATE_APP_AUTH_CLIENT } from 'graphql/mutations/create-app-auth-client';
|
||||||
|
|
||||||
import useAppConfig from 'hooks/useAppConfig.ee';
|
import useAppConfig from 'hooks/useAppConfig.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
||||||
|
|
||||||
type AdminApplicationCreateAuthClientProps = {
|
function AdminApplicationCreateAuthClient(props) {
|
||||||
appKey: string;
|
|
||||||
application: IApp;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminApplicationCreateAuthClient(
|
|
||||||
props: AdminApplicationCreateAuthClientProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { appKey, application, onClose } = props;
|
const { appKey, application, onClose } = props;
|
||||||
const { auth } = application;
|
const { auth } = application;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
@@ -37,10 +28,8 @@ export default function AdminApplicationCreateAuthClient(
|
|||||||
refetchQueries: ['GetAppAuthClients'],
|
refetchQueries: ['GetAppAuthClients'],
|
||||||
context: { autoSnackbar: false },
|
context: { autoSnackbar: false },
|
||||||
});
|
});
|
||||||
|
const submitHandler = async (values) => {
|
||||||
const submitHandler: SubmitHandler<FieldValues> = async (values) => {
|
|
||||||
let appConfigId = appConfig?.id;
|
let appConfigId = appConfig?.id;
|
||||||
|
|
||||||
if (!appConfigId) {
|
if (!appConfigId) {
|
||||||
const { data: appConfigData } = await createAppConfig({
|
const { data: appConfigData } = await createAppConfig({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -54,9 +43,7 @@ export default function AdminApplicationCreateAuthClient(
|
|||||||
});
|
});
|
||||||
appConfigId = appConfigData.createAppConfig.id;
|
appConfigId = appConfigData.createAppConfig.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, active, ...formattedAuthDefaults } = values;
|
const { name, active, ...formattedAuthDefaults } = values;
|
||||||
|
|
||||||
await createAppAuthClient({
|
await createAppAuthClient({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
@@ -67,17 +54,13 @@ export default function AdminApplicationCreateAuthClient(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAuthFieldsDefaultValues = useCallback(() => {
|
const getAuthFieldsDefaultValues = useCallback(() => {
|
||||||
if (!auth?.fields) {
|
if (!auth?.fields) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const defaultValues: {
|
const defaultValues = {};
|
||||||
[key: string]: any;
|
|
||||||
} = {};
|
|
||||||
auth.fields.forEach((field) => {
|
auth.fields.forEach((field) => {
|
||||||
if (field.value || field.type !== 'string') {
|
if (field.value || field.type !== 'string') {
|
||||||
defaultValues[field.key] = field.value;
|
defaultValues[field.key] = field.value;
|
||||||
@@ -87,16 +70,14 @@ export default function AdminApplicationCreateAuthClient(
|
|||||||
});
|
});
|
||||||
return defaultValues;
|
return defaultValues;
|
||||||
}, [auth?.fields]);
|
}, [auth?.fields]);
|
||||||
|
|
||||||
const defaultValues = useMemo(
|
const defaultValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
name: '',
|
name: '',
|
||||||
active: false,
|
active: false,
|
||||||
...getAuthFieldsDefaultValues(),
|
...getAuthFieldsDefaultValues(),
|
||||||
}),
|
}),
|
||||||
[getAuthFieldsDefaultValues]
|
[getAuthFieldsDefaultValues],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminApplicationAuthClientDialog
|
<AdminApplicationAuthClientDialog
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
@@ -110,3 +91,11 @@ export default function AdminApplicationCreateAuthClient(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AdminApplicationCreateAuthClient.propTypes = {
|
||||||
|
appKey: PropTypes.string.isRequired,
|
||||||
|
application: AppPropType.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminApplicationCreateAuthClient;
|
@@ -1,3 +1,4 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import useAppConfig from 'hooks/useAppConfig.ee';
|
import useAppConfig from 'hooks/useAppConfig.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
@@ -6,39 +7,29 @@ import Paper from '@mui/material/Paper';
|
|||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
|
|
||||||
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
||||||
import { UPDATE_APP_CONFIG } from 'graphql/mutations/update-app-config';
|
import { UPDATE_APP_CONFIG } from 'graphql/mutations/update-app-config';
|
||||||
|
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
import { Switch } from './style';
|
import { Switch } from './style';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
|
|
||||||
type AdminApplicationSettingsProps = {
|
function AdminApplicationSettings(props) {
|
||||||
appKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function AdminApplicationSettings(
|
|
||||||
props: AdminApplicationSettingsProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { appConfig, loading } = useAppConfig(props.appKey);
|
const { appConfig, loading } = useAppConfig(props.appKey);
|
||||||
const [createAppConfig, { loading: loadingCreateAppConfig }] = useMutation(
|
const [createAppConfig, { loading: loadingCreateAppConfig }] = useMutation(
|
||||||
CREATE_APP_CONFIG,
|
CREATE_APP_CONFIG,
|
||||||
{
|
{
|
||||||
refetchQueries: ['GetAppConfig'],
|
refetchQueries: ['GetAppConfig'],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const [updateAppConfig, { loading: loadingUpdateAppConfig }] = useMutation(
|
const [updateAppConfig, { loading: loadingUpdateAppConfig }] = useMutation(
|
||||||
UPDATE_APP_CONFIG,
|
UPDATE_APP_CONFIG,
|
||||||
{
|
{
|
||||||
refetchQueries: ['GetAppConfig'],
|
refetchQueries: ['GetAppConfig'],
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
const handleSubmit = async (values) => {
|
||||||
const handleSubmit = async (values: any) => {
|
|
||||||
try {
|
try {
|
||||||
if (!appConfig) {
|
if (!appConfig) {
|
||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
@@ -56,23 +47,21 @@ function AdminApplicationSettings(
|
|||||||
enqueueSnackbar(formatMessage('adminAppsSettings.successfullySaved'), {
|
enqueueSnackbar(formatMessage('adminAppsSettings.successfullySaved'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-save-admin-apps-settings-success'
|
'data-test': 'snackbar-save-admin-apps-settings-success',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error('Failed while saving!');
|
throw new Error('Failed while saving!');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = useMemo(
|
const defaultValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
allowCustomConnection: appConfig?.allowCustomConnection || false,
|
allowCustomConnection: appConfig?.allowCustomConnection || false,
|
||||||
shared: appConfig?.shared || false,
|
shared: appConfig?.shared || false,
|
||||||
disabled: appConfig?.disabled || false,
|
disabled: appConfig?.disabled || false,
|
||||||
}),
|
}),
|
||||||
[appConfig]
|
[appConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
@@ -123,4 +112,8 @@ function AdminApplicationSettings(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AdminApplicationSettings.propTypes = {
|
||||||
|
appKey: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default AdminApplicationSettings;
|
export default AdminApplicationSettings;
|
@@ -1,6 +1,5 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import SwitchBase from 'components/Switch';
|
import SwitchBase from 'components/Switch';
|
||||||
|
|
||||||
export const Switch = styled(SwitchBase)`
|
export const Switch = styled(SwitchBase)`
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 0;
|
margin: 0;
|
@@ -1,55 +1,52 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import type { IApp } from 'types';
|
|
||||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { UPDATE_APP_AUTH_CLIENT } from 'graphql/mutations/update-app-auth-client';
|
|
||||||
|
|
||||||
import useAppAuthClient from 'hooks/useAppAuthClient.ee';
|
import { AppPropType } from 'propTypes/propTypes';
|
||||||
|
import { UPDATE_APP_AUTH_CLIENT } from 'graphql/mutations/update-app-auth-client';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
||||||
|
import useAdminAppAuthClient from 'hooks/useAdminAppAuthClient.ee';
|
||||||
|
|
||||||
type AdminApplicationUpdateAuthClientProps = {
|
function AdminApplicationUpdateAuthClient(props) {
|
||||||
application: IApp;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AdminApplicationUpdateAuthClient(
|
|
||||||
props: AdminApplicationUpdateAuthClientProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { application, onClose } = props;
|
const { application, onClose } = props;
|
||||||
const { auth } = application;
|
const { auth } = application;
|
||||||
const authFields = auth?.fields?.map((field) => ({
|
|
||||||
...field,
|
|
||||||
required: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
const { clientId } = useParams();
|
const { clientId } = useParams();
|
||||||
const { appAuthClient, loading: loadingAuthClient } =
|
|
||||||
useAppAuthClient(clientId);
|
const { data: adminAppAuthClient, isLoading: isAdminAuthClientLoading } =
|
||||||
|
useAdminAppAuthClient(clientId);
|
||||||
|
|
||||||
const [updateAppAuthClient, { loading: loadingUpdateAppAuthClient, error }] =
|
const [updateAppAuthClient, { loading: loadingUpdateAppAuthClient, error }] =
|
||||||
useMutation(UPDATE_APP_AUTH_CLIENT, {
|
useMutation(UPDATE_APP_AUTH_CLIENT, {
|
||||||
refetchQueries: ['GetAppAuthClients'],
|
refetchQueries: ['GetAppAuthClients'],
|
||||||
context: { autoSnackbar: false },
|
context: { autoSnackbar: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitHandler: SubmitHandler<FieldValues> = async (values) => {
|
const authFields = auth?.fields?.map((field) => ({
|
||||||
if (!appAuthClient) {
|
...field,
|
||||||
|
required: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const submitHandler = async (values) => {
|
||||||
|
if (!adminAppAuthClient) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, active, ...formattedAuthDefaults } = values;
|
const { name, active, ...formattedAuthDefaults } = values;
|
||||||
|
|
||||||
await updateAppAuthClient({
|
await updateAppAuthClient({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
id: appAuthClient.id,
|
id: adminAppAuthClient.data.id,
|
||||||
name,
|
name,
|
||||||
active,
|
active,
|
||||||
formattedAuthDefaults,
|
formattedAuthDefaults,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,9 +54,8 @@ export default function AdminApplicationUpdateAuthClient(
|
|||||||
if (!authFields) {
|
if (!authFields) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const defaultValues: {
|
|
||||||
[key: string]: any;
|
const defaultValues = {};
|
||||||
} = {};
|
|
||||||
authFields.forEach((field) => {
|
authFields.forEach((field) => {
|
||||||
if (field.value || field.type !== 'string') {
|
if (field.value || field.type !== 'string') {
|
||||||
defaultValues[field.key] = field.value;
|
defaultValues[field.key] = field.value;
|
||||||
@@ -72,11 +68,11 @@ export default function AdminApplicationUpdateAuthClient(
|
|||||||
|
|
||||||
const defaultValues = useMemo(
|
const defaultValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
name: appAuthClient?.name || '',
|
name: adminAppAuthClient?.data?.name || '',
|
||||||
active: appAuthClient?.active || false,
|
active: adminAppAuthClient?.data?.active || false,
|
||||||
...getAuthFieldsDefaultValues(),
|
...getAuthFieldsDefaultValues(),
|
||||||
}),
|
}),
|
||||||
[appAuthClient, getAuthFieldsDefaultValues]
|
[adminAppAuthClient, getAuthFieldsDefaultValues],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,12 +80,19 @@ export default function AdminApplicationUpdateAuthClient(
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
error={error}
|
error={error}
|
||||||
title={formatMessage('updateAuthClient.title')}
|
title={formatMessage('updateAuthClient.title')}
|
||||||
loading={loadingAuthClient}
|
loading={isAdminAuthClientLoading}
|
||||||
submitHandler={submitHandler}
|
submitHandler={submitHandler}
|
||||||
authFields={authFields}
|
authFields={authFields}
|
||||||
submitting={loadingUpdateAppAuthClient}
|
submitting={loadingUpdateAppAuthClient}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
disabled={!appAuthClient}
|
disabled={!adminAppAuthClient}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AdminApplicationUpdateAuthClient.propTypes = {
|
||||||
|
application: AppPropType.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminApplicationUpdateAuthClient;
|
@@ -4,42 +4,26 @@ import GroupsIcon from '@mui/icons-material/Groups';
|
|||||||
import LockIcon from '@mui/icons-material/LockPerson';
|
import LockIcon from '@mui/icons-material/LockPerson';
|
||||||
import BrushIcon from '@mui/icons-material/Brush';
|
import BrushIcon from '@mui/icons-material/Brush';
|
||||||
import AppsIcon from '@mui/icons-material/Apps';
|
import AppsIcon from '@mui/icons-material/Apps';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { SvgIconComponent } from '@mui/icons-material';
|
|
||||||
import AppBar from 'components/AppBar';
|
import AppBar from 'components/AppBar';
|
||||||
import Drawer from 'components/Drawer';
|
import Drawer from 'components/Drawer';
|
||||||
|
import Can from 'components/Can';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
|
|
||||||
type SettingsLayoutProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DrawerLink = {
|
|
||||||
Icon: SvgIconComponent;
|
|
||||||
primary: string;
|
|
||||||
to: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function createDrawerLinks({
|
function createDrawerLinks({
|
||||||
canReadRole,
|
canReadRole,
|
||||||
canReadUser,
|
canReadUser,
|
||||||
canUpdateConfig,
|
canUpdateConfig,
|
||||||
canManageSamlAuthProvider,
|
canManageSamlAuthProvider,
|
||||||
canUpdateApp,
|
canUpdateApp,
|
||||||
}: {
|
|
||||||
canReadRole: boolean;
|
|
||||||
canReadUser: boolean;
|
|
||||||
canUpdateConfig: boolean;
|
|
||||||
canManageSamlAuthProvider: boolean;
|
|
||||||
canUpdateApp: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const items = [
|
const items = [
|
||||||
canReadUser
|
canReadUser
|
||||||
@@ -82,20 +66,16 @@ function createDrawerLinks({
|
|||||||
dataTest: 'apps-drawer-link',
|
dataTest: 'apps-drawer-link',
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
].filter(Boolean) as DrawerLink[];
|
].filter(Boolean);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsLayout({
|
function SettingsLayout() {
|
||||||
children,
|
|
||||||
}: SettingsLayoutProps): React.ReactElement {
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const currentUserAbility = useCurrentUserAbility();
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||||
|
|
||||||
const openDrawer = () => setDrawerOpen(true);
|
const openDrawer = () => setDrawerOpen(true);
|
||||||
const closeDrawer = () => setDrawerOpen(false);
|
const closeDrawer = () => setDrawerOpen(false);
|
||||||
const drawerLinks = createDrawerLinks({
|
const drawerLinks = createDrawerLinks({
|
||||||
@@ -108,7 +88,6 @@ export default function SettingsLayout({
|
|||||||
currentUserAbility.can('create', 'SamlAuthProvider'),
|
currentUserAbility.can('create', 'SamlAuthProvider'),
|
||||||
canUpdateApp: currentUserAbility.can('update', 'App'),
|
canUpdateApp: currentUserAbility.can('update', 'App'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const drawerBottomLinks = [
|
const drawerBottomLinks = [
|
||||||
{
|
{
|
||||||
Icon: ArrowBackIosNewIcon,
|
Icon: ArrowBackIosNewIcon,
|
||||||
@@ -117,15 +96,13 @@ export default function SettingsLayout({
|
|||||||
dataTest: 'go-back-drawer-link',
|
dataTest: 'go-back-drawer-link',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Can I="read" a="User">
|
||||||
<AppBar
|
<AppBar
|
||||||
drawerOpen={isDrawerOpen}
|
drawerOpen={isDrawerOpen}
|
||||||
onDrawerOpen={openDrawer}
|
onDrawerOpen={openDrawer}
|
||||||
onDrawerClose={closeDrawer}
|
onDrawerClose={closeDrawer}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
<Drawer
|
<Drawer
|
||||||
links={drawerLinks}
|
links={drawerLinks}
|
||||||
@@ -134,13 +111,13 @@ export default function SettingsLayout({
|
|||||||
onOpen={openDrawer}
|
onOpen={openDrawer}
|
||||||
onClose={closeDrawer}
|
onClose={closeDrawer}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
|
<Outlet />
|
||||||
{children}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</Can>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default SettingsLayout;
|
@@ -5,11 +5,7 @@ import { mutateAndGetClient } from 'graphql/client';
|
|||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
|
|
||||||
type ApolloProviderProps = {
|
const ApolloProvider = (props) => {
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
|
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
|
|
||||||
@@ -18,11 +14,11 @@ const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
|
|||||||
enqueueSnackbar(message, {
|
enqueueSnackbar(message, {
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
'data-test': 'snackbar-error'
|
'data-test': 'snackbar-error',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[enqueueSnackbar]
|
[enqueueSnackbar],
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = React.useMemo(() => {
|
const client = React.useMemo(() => {
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user