Compare commits
68 Commits
helix-new-
...
custom-see
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7dee26c16d | ||
![]() |
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 |
@@ -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,7 @@ ENV PORT 3000
|
|||||||
RUN \
|
RUN \
|
||||||
apk --no-cache add --virtual build-dependencies python3 build-base git
|
apk --no-cache add --virtual build-dependencies python3 build-base git
|
||||||
|
|
||||||
RUN git clone -b helix-new-endpoint https://github.com/automatisch/automatisch.git
|
RUN git clone https://github.com/automatisch/automatisch.git
|
||||||
|
|
||||||
WORKDIR /automatisch
|
WORKDIR /automatisch
|
||||||
|
|
||||||
|
@@ -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",
|
||||||
|
@@ -18,8 +18,8 @@ async function fetchAdminRole() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createUser(
|
export async function createUser(
|
||||||
email = 'user@automatisch.io',
|
email = appConfig.seedUserEmail,
|
||||||
password = 'sample'
|
password = appConfig.seedUserPassword
|
||||||
) {
|
) {
|
||||||
const UNIQUE_VIOLATION_CODE = '23505';
|
const UNIQUE_VIOLATION_CODE = '23505';
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
|
@@ -5,19 +5,38 @@ 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 response = await $.http.post('/api/v1/sessions/chat', {
|
const response = await $.http.post('/api/v1/sessions/chat', {
|
||||||
session_id: '',
|
session_id: $.step.parameters.sessionId,
|
||||||
|
system: $.step.parameters.systemPrompt,
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
|
@@ -94,6 +94,8 @@ const appConfig = {
|
|||||||
disableFavicon: process.env.DISABLE_FAVICON === 'true',
|
disableFavicon: process.env.DISABLE_FAVICON === 'true',
|
||||||
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
|
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
|
||||||
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
|
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
|
||||||
|
seedUserEmail: process.env.SEED_USER_EMAIL || 'user@automatisch.io',
|
||||||
|
seedUserPassword: process.env.SEED_USER_PASSWORD || 'sample',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!appConfig.encryptionKey) {
|
if (!appConfig.encryptionKey) {
|
||||||
|
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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) => {
|
||||||
|
@@ -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);
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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)
|
||||||
|
@@ -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"]
|
||||||
|
}
|
@@ -13,17 +13,10 @@
|
|||||||
"@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 +24,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 +35,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 +46,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 +79,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(() => {
|
@@ -1,3 +1,4 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import List from '@mui/material/List';
|
import List from '@mui/material/List';
|
||||||
@@ -5,33 +6,23 @@ import ListItem from '@mui/material/ListItem';
|
|||||||
import ListItemButton from '@mui/material/ListItemButton';
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
type AppAuthClientsDialogProps = {
|
function AppAuthClientsDialog(props) {
|
||||||
appKey: string;
|
|
||||||
onClientClick: (appAuthClientId: string) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AppAuthClientsDialog(props: AppAuthClientsDialogProps) {
|
|
||||||
const { appKey, onClientClick, onClose } = props;
|
const { appKey, onClientClick, onClose } = props;
|
||||||
const { appAuthClients } = useAppAuthClients({ appKey, active: true });
|
const { appAuthClients } = useAppAuthClients({ appKey, active: true });
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function autoAuthenticateSingleClient() {
|
function autoAuthenticateSingleClient() {
|
||||||
if (appAuthClients?.length === 1) {
|
if (appAuthClients?.length === 1) {
|
||||||
onClientClick(appAuthClients[0].id);
|
onClientClick(appAuthClients[0].id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[appAuthClients]
|
[appAuthClients],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!appAuthClients?.length || appAuthClients?.length === 1)
|
if (!appAuthClients?.length || appAuthClients?.length === 1)
|
||||||
return <React.Fragment />;
|
return <React.Fragment />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onClose={onClose} open={true}>
|
<Dialog onClose={onClose} open={true}>
|
||||||
<DialogTitle>{formatMessage('appAuthClientsDialog.title')}</DialogTitle>
|
<DialogTitle>{formatMessage('appAuthClientsDialog.title')}</DialogTitle>
|
||||||
@@ -48,3 +39,11 @@ export default function AppAuthClientsDialog(props: AppAuthClientsDialogProps) {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppAuthClientsDialog.propTypes = {
|
||||||
|
appKey: PropTypes.string.isRequired,
|
||||||
|
onClientClick: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppAuthClientsDialog;
|
@@ -1,50 +1,35 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
import MenuOpenIcon from '@mui/icons-material/MenuOpen';
|
import MenuOpenIcon from '@mui/icons-material/MenuOpen';
|
||||||
import MuiAppBar from '@mui/material/AppBar';
|
import MuiAppBar from '@mui/material/AppBar';
|
||||||
import type { ContainerProps } from '@mui/material/Container';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
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 AccountDropdownMenu from 'components/AccountDropdownMenu';
|
import AccountDropdownMenu from 'components/AccountDropdownMenu';
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import Logo from 'components/Logo/index';
|
import Logo from 'components/Logo/index';
|
||||||
import TrialStatusBadge from 'components/TrialStatusBadge/index.ee';
|
import TrialStatusBadge from 'components/TrialStatusBadge/index.ee';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
|
||||||
import { Link } from './style';
|
import { Link } from './style';
|
||||||
|
|
||||||
type AppBarProps = {
|
|
||||||
drawerOpen: boolean;
|
|
||||||
onDrawerOpen: () => void;
|
|
||||||
onDrawerClose: () => void;
|
|
||||||
maxWidth?: ContainerProps['maxWidth'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const accountMenuId = 'account-menu';
|
const accountMenuId = 'account-menu';
|
||||||
|
|
||||||
export default function AppBar(props: AppBarProps): React.ReactElement {
|
function AppBar(props) {
|
||||||
const { drawerOpen, onDrawerOpen, onDrawerClose, maxWidth = false } = props;
|
const { drawerOpen, onDrawerOpen, onDrawerClose, maxWidth = false } = props;
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
const [accountMenuAnchorElement, setAccountMenuAnchorElement] =
|
const [accountMenuAnchorElement, setAccountMenuAnchorElement] =
|
||||||
React.useState<null | HTMLElement>(null);
|
React.useState(null);
|
||||||
|
|
||||||
const isMenuOpen = Boolean(accountMenuAnchorElement);
|
const isMenuOpen = Boolean(accountMenuAnchorElement);
|
||||||
|
const handleAccountMenuOpen = (event) => {
|
||||||
const handleAccountMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAccountMenuAnchorElement(event.currentTarget);
|
setAccountMenuAnchorElement(event.currentTarget);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAccountMenuClose = () => {
|
const handleAccountMenuClose = () => {
|
||||||
setAccountMenuAnchorElement(null);
|
setAccountMenuAnchorElement(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MuiAppBar data-test="app-bar">
|
<MuiAppBar data-test="app-bar">
|
||||||
<Container maxWidth={maxWidth} disableGutters>
|
<Container maxWidth={maxWidth} disableGutters>
|
||||||
@@ -91,3 +76,15 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
|
|||||||
</MuiAppBar>
|
</MuiAppBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppBar.propTypes = {
|
||||||
|
drawerOpen: PropTypes.bool.isRequired,
|
||||||
|
onDrawerOpen: PropTypes.func.isRequired,
|
||||||
|
onDrawerClose: PropTypes.func.isRequired,
|
||||||
|
maxWidth: PropTypes.oneOfType([
|
||||||
|
PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl', false]),
|
||||||
|
PropTypes.string,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppBar;
|
@@ -1,6 +1,5 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
|
||||||
export const Link = styled(RouterLink)(() => ({
|
export const Link = styled(RouterLink)(() => ({
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: 'inherit',
|
color: 'inherit',
|
@@ -1,29 +1,14 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
import type { PopoverProps } from '@mui/material/Popover';
|
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import type { IConnection } from 'types';
|
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import { ConnectionPropType } from 'propTypes/propTypes';
|
||||||
|
|
||||||
type Action = {
|
function ContextMenu(props) {
|
||||||
type: 'test' | 'reconnect' | 'delete' | 'viewFlows';
|
|
||||||
};
|
|
||||||
|
|
||||||
type ContextMenuProps = {
|
|
||||||
appKey: string;
|
|
||||||
connection: IConnection;
|
|
||||||
onClose: () => void;
|
|
||||||
onMenuItemClick: (event: React.MouseEvent, action: Action) => void;
|
|
||||||
anchorEl: PopoverProps['anchorEl'];
|
|
||||||
disableReconnection: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ContextMenu(
|
|
||||||
props: ContextMenuProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const {
|
const {
|
||||||
appKey,
|
appKey,
|
||||||
connection,
|
connection,
|
||||||
@@ -33,18 +18,15 @@ export default function ContextMenu(
|
|||||||
disableReconnection,
|
disableReconnection,
|
||||||
} = props;
|
} = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
|
||||||
const createActionHandler = React.useCallback(
|
const createActionHandler = React.useCallback(
|
||||||
(action: Action) => {
|
(action) => {
|
||||||
return function clickHandler(event: React.MouseEvent) {
|
return function clickHandler(event) {
|
||||||
onMenuItemClick(event, action);
|
onMenuItemClick(event, action);
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[onMenuItemClick, onClose]
|
[onMenuItemClick, onClose],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
open={true}
|
open={true}
|
||||||
@@ -70,7 +52,7 @@ export default function ContextMenu(
|
|||||||
to={URLS.APP_RECONNECT_CONNECTION(
|
to={URLS.APP_RECONNECT_CONNECTION(
|
||||||
appKey,
|
appKey,
|
||||||
connection.id,
|
connection.id,
|
||||||
connection.appAuthClientId
|
connection.appAuthClientId,
|
||||||
)}
|
)}
|
||||||
onClick={createActionHandler({ type: 'reconnect' })}
|
onClick={createActionHandler({ type: 'reconnect' })}
|
||||||
>
|
>
|
||||||
@@ -83,3 +65,17 @@ export default function ContextMenu(
|
|||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ContextMenu.propTypes = {
|
||||||
|
appKey: PropTypes.string.isRequired,
|
||||||
|
connection: ConnectionPropType.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
onMenuItemClick: PropTypes.func.isRequired,
|
||||||
|
anchorEl: PropTypes.oneOfType([
|
||||||
|
PropTypes.func,
|
||||||
|
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||||
|
]),
|
||||||
|
disableReconnection: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContextMenu;
|
@@ -10,26 +10,20 @@ import Stack from '@mui/material/Stack';
|
|||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import type { IConnection } from 'types';
|
|
||||||
import ConnectionContextMenu from 'components/AppConnectionContextMenu';
|
import ConnectionContextMenu from 'components/AppConnectionContextMenu';
|
||||||
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
|
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
|
||||||
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import { ConnectionPropType } from 'propTypes/propTypes';
|
||||||
import { CardContent, Typography } from './style';
|
import { CardContent, Typography } from './style';
|
||||||
|
|
||||||
type AppConnectionRowProps = {
|
const countTranslation = (value) => (
|
||||||
connection: IConnection;
|
|
||||||
};
|
|
||||||
|
|
||||||
const countTranslation = (value: React.ReactNode) => (
|
|
||||||
<>
|
<>
|
||||||
<Typography variant="body1">{value}</Typography>
|
<Typography variant="body1">{value}</Typography>
|
||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
function AppConnectionRow(props) {
|
||||||
function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
const [verificationVisible, setVerificationVisible] = React.useState(false);
|
const [verificationVisible, setVerificationVisible] = React.useState(false);
|
||||||
const [testConnection, { called: testCalled, loading: testLoading }] =
|
const [testConnection, { called: testCalled, loading: testLoading }] =
|
||||||
@@ -43,7 +37,6 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [deleteConnection] = useMutation(DELETE_CONNECTION);
|
const [deleteConnection] = useMutation(DELETE_CONNECTION);
|
||||||
|
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@@ -54,17 +47,14 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
flowCount,
|
flowCount,
|
||||||
reconnectable,
|
reconnectable,
|
||||||
} = props.connection;
|
} = props.connection;
|
||||||
|
const contextButtonRef = React.useRef(null);
|
||||||
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
|
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
|
||||||
const onContextMenuAction = React.useCallback(
|
const onContextMenuAction = React.useCallback(
|
||||||
async (event, action: { [key: string]: string }) => {
|
async (event, action) => {
|
||||||
if (action.type === 'delete') {
|
if (action.type === 'delete') {
|
||||||
await deleteConnection({
|
await deleteConnection({
|
||||||
variables: { input: { id } },
|
variables: { input: { id } },
|
||||||
@@ -73,13 +63,11 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
__typename: 'Connection',
|
__typename: 'Connection',
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
cache.evict({
|
cache.evict({
|
||||||
id: connectionCacheId,
|
id: connectionCacheId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
|
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
SnackbarProps: {
|
SnackbarProps: {
|
||||||
@@ -91,13 +79,11 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
testConnection({ variables: { id } });
|
testConnection({ variables: { id } });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]
|
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar],
|
||||||
);
|
);
|
||||||
|
|
||||||
const relativeCreatedAt = DateTime.fromMillis(
|
const relativeCreatedAt = DateTime.fromMillis(
|
||||||
parseInt(createdAt, 10)
|
parseInt(createdAt, 10),
|
||||||
).toRelative();
|
).toRelative();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card sx={{ my: 2 }} data-test="app-connection-row">
|
<Card sx={{ my: 2 }} data-test="app-connection-row">
|
||||||
@@ -125,14 +111,17 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{verificationVisible && testCalled && !testLoading && verified && (
|
{verificationVisible &&
|
||||||
<>
|
testCalled &&
|
||||||
<CheckCircleIcon fontSize="small" color="success" />
|
!testLoading &&
|
||||||
<Typography variant="caption">
|
verified && (
|
||||||
{formatMessage('connection.testSuccessful')}
|
<>
|
||||||
</Typography>
|
<CheckCircleIcon fontSize="small" color="success" />
|
||||||
</>
|
<Typography variant="caption">
|
||||||
)}
|
{formatMessage('connection.testSuccessful')}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{verificationVisible &&
|
{verificationVisible &&
|
||||||
testCalled &&
|
testCalled &&
|
||||||
!testLoading &&
|
!testLoading &&
|
||||||
@@ -180,4 +169,8 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppConnectionRow.propTypes = {
|
||||||
|
connection: ConnectionPropType.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default AppConnectionRow;
|
export default AppConnectionRow;
|
@@ -1,7 +1,6 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import MuiCardContent from '@mui/material/CardContent';
|
import MuiCardContent from '@mui/material/CardContent';
|
||||||
import MuiTypography from '@mui/material/Typography';
|
import MuiTypography from '@mui/material/Typography';
|
||||||
|
|
||||||
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateRows: 'auto',
|
gridTemplateRows: 'auto',
|
||||||
@@ -9,7 +8,6 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
|||||||
gridColumnGap: theme.spacing(2),
|
gridColumnGap: theme.spacing(2),
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const Typography = styled(MuiTypography)(() => ({
|
export const Typography = styled(MuiTypography)(() => ({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
@@ -1,29 +1,20 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
|
|
||||||
import type { IConnection } from 'types';
|
|
||||||
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
||||||
import AppConnectionRow from 'components/AppConnectionRow';
|
import AppConnectionRow from 'components/AppConnectionRow';
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
|
|
||||||
type AppConnectionsProps = {
|
function AppConnections(props) {
|
||||||
appKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AppConnections(
|
|
||||||
props: AppConnectionsProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { appKey } = props;
|
const { appKey } = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { data } = useQuery(GET_APP_CONNECTIONS, {
|
const { data } = useQuery(GET_APP_CONNECTIONS, {
|
||||||
variables: { key: appKey },
|
variables: { key: appKey },
|
||||||
});
|
});
|
||||||
const appConnections: IConnection[] = data?.getApp?.connections || [];
|
const appConnections = data?.getApp?.connections || [];
|
||||||
|
|
||||||
const hasConnections = appConnections?.length;
|
const hasConnections = appConnections?.length;
|
||||||
|
|
||||||
if (!hasConnections) {
|
if (!hasConnections) {
|
||||||
return (
|
return (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
@@ -36,9 +27,15 @@ export default function AppConnections(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{appConnections.map((appConnection: IConnection) => (
|
{appConnections.map((appConnection) => (
|
||||||
<AppConnectionRow key={appConnection.id} connection={appConnection} />
|
<AppConnectionRow key={appConnection.id} connection={appConnection} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppConnections.propTypes = {
|
||||||
|
appKey: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppConnections;
|
@@ -1,27 +1,19 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { GET_FLOWS } from 'graphql/queries/get-flows';
|
import { GET_FLOWS } from 'graphql/queries/get-flows';
|
||||||
import Pagination from '@mui/material/Pagination';
|
import Pagination from '@mui/material/Pagination';
|
||||||
import PaginationItem from '@mui/material/PaginationItem';
|
import PaginationItem from '@mui/material/PaginationItem';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import AppFlowRow from 'components/FlowRow';
|
import AppFlowRow from 'components/FlowRow';
|
||||||
import NoResultFound from 'components/NoResultFound';
|
import NoResultFound from 'components/NoResultFound';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import type { IFlow } from 'types';
|
|
||||||
|
|
||||||
type AppFlowsProps = {
|
|
||||||
appKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FLOW_PER_PAGE = 10;
|
const FLOW_PER_PAGE = 10;
|
||||||
|
const getLimitAndOffset = (page) => ({
|
||||||
const getLimitAndOffset = (page: number) => ({
|
|
||||||
limit: FLOW_PER_PAGE,
|
limit: FLOW_PER_PAGE,
|
||||||
offset: (page - 1) * FLOW_PER_PAGE,
|
offset: (page - 1) * FLOW_PER_PAGE,
|
||||||
});
|
});
|
||||||
|
function AppFlows(props) {
|
||||||
export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
|
||||||
const { appKey } = props;
|
const { appKey } = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
@@ -36,10 +28,8 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
|||||||
});
|
});
|
||||||
const getFlows = data?.getFlows || {};
|
const getFlows = data?.getFlows || {};
|
||||||
const { pageInfo, edges } = getFlows;
|
const { pageInfo, edges } = getFlows;
|
||||||
|
const flows = edges?.map(({ node }) => node);
|
||||||
const flows: IFlow[] = edges?.map(({ node }: { node: IFlow }) => node);
|
|
||||||
const hasFlows = flows?.length;
|
const hasFlows = flows?.length;
|
||||||
|
|
||||||
if (!hasFlows) {
|
if (!hasFlows) {
|
||||||
return (
|
return (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
@@ -49,10 +39,9 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{flows?.map((appFlow: IFlow) => (
|
{flows?.map((appFlow) => (
|
||||||
<AppFlowRow key={appFlow.id} flow={appFlow} />
|
<AppFlowRow key={appFlow.id} flow={appFlow} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -74,3 +63,9 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppFlows.propTypes = {
|
||||||
|
appKey: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppFlows;
|
@@ -1,25 +1,12 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from '@mui/material/Avatar';
|
||||||
import type { AvatarProps } from '@mui/material/Avatar';
|
const inlineImgStyle = {
|
||||||
|
|
||||||
type AppIconProps = {
|
|
||||||
name?: string;
|
|
||||||
url?: string;
|
|
||||||
color?: string;
|
|
||||||
variant?: AvatarProps['variant'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const inlineImgStyle: React.CSSProperties = {
|
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
};
|
};
|
||||||
|
function AppIcon(props) {
|
||||||
export default function AppIcon(
|
|
||||||
props: AppIconProps & AvatarProps
|
|
||||||
): React.ReactElement {
|
|
||||||
const { name, url, color, sx = {}, variant = 'square', ...restProps } = props;
|
const { name, url, color, sx = {}, variant = 'square', ...restProps } = props;
|
||||||
|
|
||||||
const initialLetter = name?.[0];
|
const initialLetter = name?.[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
component="span"
|
component="span"
|
||||||
@@ -28,8 +15,22 @@ export default function AppIcon(
|
|||||||
imgProps={{ style: inlineImgStyle }}
|
imgProps={{ style: inlineImgStyle }}
|
||||||
src={url}
|
src={url}
|
||||||
alt={name}
|
alt={name}
|
||||||
children={initialLetter}
|
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
>
|
||||||
|
{initialLetter}
|
||||||
|
</Avatar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppIcon.propTypes = {
|
||||||
|
name: PropTypes.string,
|
||||||
|
url: PropTypes.string,
|
||||||
|
color: PropTypes.string,
|
||||||
|
variant: PropTypes.oneOfType([
|
||||||
|
PropTypes.oneOf(['circular', 'rounded', 'square']),
|
||||||
|
PropTypes.string,
|
||||||
|
]),
|
||||||
|
sx: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppIcon;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user