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
|
||||
echo "
|
||||
PORT=$WEB_PORT
|
||||
REACT_APP_GRAPHQL_URL=http://localhost:$BACKEND_PORT/graphql
|
||||
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
||||
" >> .env
|
||||
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 workflow is now ready to test your code on the runner."
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: yarn lint
|
||||
- run: cd packages/backend && yarn lint
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||
start-backend-server:
|
||||
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
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Build Automatisch
|
||||
run: yarn lerna run --scope=@*/{web,cli} build
|
||||
- name: Build Automatisch web
|
||||
working-directory: ./packages/web
|
||||
run: yarn build
|
||||
env:
|
||||
# Keep this until we clean up warnings in build processes
|
||||
CI: false
|
||||
|
@@ -6,7 +6,7 @@ ENV PORT 3000
|
||||
RUN \
|
||||
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
|
||||
|
||||
|
@@ -6,7 +6,6 @@
|
||||
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
||||
"start:web": "lerna run --stream --scope=@*/web dev",
|
||||
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
||||
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend} lint",
|
||||
"build:docs": "cd ./packages/docs && yarn install && yarn build"
|
||||
},
|
||||
"workspaces": {
|
||||
@@ -21,8 +20,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
||||
"@typescript-eslint/parser": "^5.9.1",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
|
@@ -18,8 +18,8 @@ async function fetchAdminRole() {
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
email = 'user@automatisch.io',
|
||||
password = 'sample'
|
||||
email = appConfig.seedUserEmail,
|
||||
password = appConfig.seedUserPassword
|
||||
) {
|
||||
const UNIQUE_VIOLATION_CODE = '23505';
|
||||
|
||||
|
@@ -11,7 +11,7 @@
|
||||
"start:worker": "node src/worker.js",
|
||||
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
|
||||
"test": "APP_ENV=test vitest run",
|
||||
"lint": "eslint . --ignore-path ../../.eslintignore",
|
||||
"lint": "eslint .",
|
||||
"db:create": "node ./bin/database/create.js",
|
||||
"db:seed:user": "node ./bin/database/seed-user.js",
|
||||
"db:drop": "node ./bin/database/drop.js",
|
||||
@@ -95,7 +95,6 @@
|
||||
"url": "https://github.com/automatisch/automatisch/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/utils": "^7.0.2",
|
||||
"nodemon": "^2.0.13",
|
||||
"supertest": "^6.3.3",
|
||||
"vitest": "^1.1.3"
|
||||
|
@@ -5,19 +5,38 @@ export default defineAction({
|
||||
key: 'newChat',
|
||||
description: 'Create a new chat session for Helix AI.',
|
||||
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',
|
||||
key: 'input',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Prompt to start the chat with.',
|
||||
description: 'User input to start the chat with.',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const response = await $.http.post('/api/v1/sessions/chat', {
|
||||
session_id: '',
|
||||
session_id: $.step.parameters.sessionId,
|
||||
system: $.step.parameters.systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
|
@@ -94,6 +94,8 @@ const appConfig = {
|
||||
disableFavicon: process.env.DISABLE_FAVICON === 'true',
|
||||
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
|
||||
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) {
|
||||
|
@@ -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 app from '../../../../app.js';
|
||||
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 getCurrentUserMock from '../../../../../test/mocks/rest/api/v1/users/get-current-user';
|
||||
|
||||
describe('GET /api/v1/users/me', () => {
|
||||
let role, currentUser, token;
|
||||
let role, permissionOne, permissionTwo, currentUser, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUser = await createUser();
|
||||
role = await currentUser.$relatedQuery('role');
|
||||
role = await createRole();
|
||||
|
||||
permissionOne = await createPermission({
|
||||
roleId: role.id,
|
||||
});
|
||||
|
||||
permissionTwo = await createPermission({
|
||||
roleId: role.id,
|
||||
});
|
||||
|
||||
currentUser = await createUser({
|
||||
roleId: role.id,
|
||||
});
|
||||
|
||||
token = createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
@@ -20,7 +34,11 @@ describe('GET /api/v1/users/me', () => {
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = getCurrentUserMock(currentUser, role);
|
||||
const expectedPayload = getCurrentUserMock(currentUser, role, [
|
||||
permissionOne,
|
||||
permissionTwo,
|
||||
]);
|
||||
|
||||
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 getAppConfig from './queries/get-app-config.ee.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 getConfig from './queries/get-config.ee.js';
|
||||
import getConnectedApps from './queries/get-connected-apps.js';
|
||||
@@ -39,7 +38,6 @@ const queryResolvers = {
|
||||
getAppAuthClients,
|
||||
getAppConfig,
|
||||
getApps,
|
||||
getAutomatischInfo,
|
||||
getBillingAndUsage,
|
||||
getConfig,
|
||||
getConnectedApps,
|
||||
|
@@ -40,7 +40,6 @@ type Query {
|
||||
key: String!
|
||||
parameters: JSONObject
|
||||
): [SubstepArgument]
|
||||
getAutomatischInfo: GetAutomatischInfo
|
||||
getBillingAndUsage: GetBillingAndUsage
|
||||
getCurrentUser: User
|
||||
getConfig(keys: [String]): JSONObject
|
||||
@@ -644,12 +643,6 @@ type AppHealth {
|
||||
version: String
|
||||
}
|
||||
|
||||
type GetAutomatischInfo {
|
||||
isCloud: Boolean
|
||||
isMation: Boolean
|
||||
license: License
|
||||
}
|
||||
|
||||
type License {
|
||||
id: String
|
||||
name: String
|
||||
|
@@ -42,7 +42,6 @@ const isAuthenticatedRule = rule()(isAuthenticated);
|
||||
export const authenticationRules = {
|
||||
Query: {
|
||||
'*': isAuthenticatedRule,
|
||||
getAutomatischInfo: allow,
|
||||
getConfig: allow,
|
||||
getNotifications: allow,
|
||||
healthcheck: allow,
|
||||
|
@@ -11,6 +11,18 @@ const authorizationList = {
|
||||
action: 'read',
|
||||
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) => {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import logger from './logger.js';
|
||||
import objection from 'objection';
|
||||
import * as Sentry from './sentry.ee.js';
|
||||
const { NotFoundError, DataError } = objection;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
logger.error(error.message + '\n' + error.stack);
|
||||
response.status(error.statusCode || 500).end();
|
||||
const statusCode = error.statusCode || 500;
|
||||
|
||||
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) => {
|
||||
|
@@ -3,10 +3,13 @@ import * as Tracing from '@sentry/tracing';
|
||||
|
||||
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) {
|
||||
if (!isSentryEnabled) return;
|
||||
if (!isSentryEnabled()) return;
|
||||
|
||||
return Sentry.init({
|
||||
enabled: !!appConfig.sentryDsn,
|
||||
@@ -21,19 +24,19 @@ export function init(app) {
|
||||
}
|
||||
|
||||
export function attachRequestHandler(app) {
|
||||
if (!isSentryEnabled) return;
|
||||
if (!isSentryEnabled()) return;
|
||||
|
||||
app.use(Sentry.Handlers.requestHandler());
|
||||
}
|
||||
|
||||
export function attachTracingHandler(app) {
|
||||
if (!isSentryEnabled) return;
|
||||
if (!isSentryEnabled()) return;
|
||||
|
||||
app.use(Sentry.Handlers.tracingHandler());
|
||||
}
|
||||
|
||||
export function attachErrorHandler(app) {
|
||||
if (!isSentryEnabled) return;
|
||||
if (!isSentryEnabled()) return;
|
||||
|
||||
app.use(
|
||||
Sentry.Handlers.errorHandler({
|
||||
@@ -46,7 +49,7 @@ export function attachErrorHandler(app) {
|
||||
}
|
||||
|
||||
export function captureException(exception, captureContext) {
|
||||
if (!isSentryEnabled) return;
|
||||
if (!isSentryEnabled()) return;
|
||||
|
||||
return Sentry.captureException(exception, captureContext);
|
||||
}
|
||||
|
@@ -39,6 +39,47 @@ class App {
|
||||
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) {
|
||||
const app = await this.findOneByKey(appKey);
|
||||
|
||||
|
@@ -149,6 +149,13 @@ class User extends Base {
|
||||
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) {
|
||||
return bcrypt.compare(password, this.password);
|
||||
}
|
||||
|
@@ -2,9 +2,41 @@ import { Router } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { authenticateUser } from '../../../helpers/authentication.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();
|
||||
|
||||
router.get('/', authenticateUser, asyncHandler(getAppsAction));
|
||||
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;
|
||||
|
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 flowsRouter from './api/v1/flows.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 rolesRouter from './api/v1/admin/roles.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/flows', flowsRouter);
|
||||
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/roles', rolesRouter);
|
||||
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';
|
||||
|
||||
describe('appSerializer', () => {
|
||||
it('should return permission data', async () => {
|
||||
it('should return app data', async () => {
|
||||
const app = await App.findOneByKey('deepl');
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
if (flow.steps) {
|
||||
if (flow.steps?.length > 0) {
|
||||
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 stepSerializer from './step.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 = {
|
||||
User: userSerializer,
|
||||
@@ -16,6 +21,11 @@ const serializers = {
|
||||
Flow: flowSerializer,
|
||||
Step: stepSerializer,
|
||||
App: appSerializer,
|
||||
Auth: authSerializer,
|
||||
Trigger: triggerSerializer,
|
||||
Action: actionSerializer,
|
||||
Execution: executionSerializer,
|
||||
ExecutionStep: executionStepSerializer,
|
||||
};
|
||||
|
||||
export default serializers;
|
||||
|
@@ -11,7 +11,7 @@ const roleSerializer = (role) => {
|
||||
isAdmin: role.isAdmin,
|
||||
};
|
||||
|
||||
if (role.permissions) {
|
||||
if (role.permissions?.length > 0) {
|
||||
roleData.permissions = role.permissions.map((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);
|
||||
}
|
||||
|
||||
if (user.permissions) {
|
||||
if (user.permissions?.length > 0) {
|
||||
userData.permissions = user.permissions.map((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 {
|
||||
data: {
|
||||
createdAt: currentUser.createdAt.getTime(),
|
||||
email: currentUser.email,
|
||||
fullName: currentUser.fullName,
|
||||
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: {
|
||||
createdAt: role.createdAt.getTime(),
|
||||
description: null,
|
||||
|
@@ -3,6 +3,7 @@
|
||||
The following integrations are currently supported by Automatisch.
|
||||
|
||||
- [Carbone](/apps/carbone/actions)
|
||||
- [Datastore](/apps/datastore/actions)
|
||||
- [DeepL](/apps/deepl/actions)
|
||||
- [Delay](/apps/delay/actions)
|
||||
- [Discord](/apps/discord/actions)
|
||||
|
@@ -31,7 +31,7 @@ export class AdminRolesPage extends AuthenticatedPage {
|
||||
await this.roleDrawerLink.click();
|
||||
await this.isMounted();
|
||||
await this.rolesLoader.waitFor({
|
||||
state: 'detached'
|
||||
state: 'detached',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,9 +43,7 @@ export class AdminRolesPage extends AuthenticatedPage {
|
||||
state: 'detached',
|
||||
});
|
||||
return this.roleRow.filter({
|
||||
has: this.page.getByTestId('role-name').filter({
|
||||
hasText: name,
|
||||
}),
|
||||
has: this.page.getByTestId('role-name').getByText(name, { exact: true }),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -28,8 +28,6 @@
|
||||
"@playwright/test": "^1.36.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
||||
"@typescript-eslint/parser": "^5.9.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.13.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
|
@@ -197,7 +197,7 @@ test.describe('Role management page', () => {
|
||||
await adminCreateUserPage.passwordInput.fill('sample');
|
||||
await adminCreateUserPage.roleInput.click();
|
||||
await adminCreateUserPage.page
|
||||
.getByRole('option', { name: 'Delete Role' })
|
||||
.getByRole('option', { name: 'Delete Role', exact: true })
|
||||
.click();
|
||||
await adminCreateUserPage.createButton.click();
|
||||
await adminUsersPage.snackbar.waitFor({
|
||||
|
@@ -1,4 +1,4 @@
|
||||
PORT=3001
|
||||
REACT_APP_GRAPHQL_URL=http://localhost:3000/graphql
|
||||
REACT_APP_BACKEND_URL=http://localhost:3000
|
||||
# HTTPS=true
|
||||
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/lab": "^5.0.0-alpha.120",
|
||||
"@mui/material": "^5.11.10",
|
||||
"@tanstack/react-query": "^5.24.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@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",
|
||||
"compare-versions": "^4.1.3",
|
||||
"graphql": "^15.6.0",
|
||||
@@ -31,8 +24,8 @@
|
||||
"luxon": "^2.3.1",
|
||||
"mui-color-input": "^2.0.0",
|
||||
"notistack": "^3.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.45.2",
|
||||
"react-intl": "^5.20.12",
|
||||
"react-json-tree": "^0.16.2",
|
||||
@@ -42,7 +35,6 @@
|
||||
"slate": "^0.94.1",
|
||||
"slate-history": "^0.93.0",
|
||||
"slate-react": "^0.94.2",
|
||||
"typescript": "^4.6.3",
|
||||
"uuid": "^9.0.0",
|
||||
"web-vitals": "^1.0.1",
|
||||
"yup": "^0.32.11"
|
||||
@@ -54,8 +46,8 @@
|
||||
"build:watch": "yarn nodemon --exec react-scripts build --watch 'src/**/*.ts' --watch 'public/**/*' --ext ts,html",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint . --ignore-path ../../.eslintignore",
|
||||
"prepack": "REACT_APP_GRAPHQL_URL=/graphql yarn build"
|
||||
"lint": "eslint src --ext .js,.jsx",
|
||||
"prepack": "yarn build"
|
||||
},
|
||||
"files": [
|
||||
"/build"
|
||||
@@ -87,5 +79,17 @@
|
||||
},
|
||||
"publishConfig": {
|
||||
"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 AdminSettingsLayout from 'components/AdminSettingsLayout';
|
||||
|
||||
import Users from 'pages/Users';
|
||||
import EditUser from 'pages/EditUser';
|
||||
import CreateUser from 'pages/CreateUser';
|
||||
@@ -8,12 +8,10 @@ import CreateRole from 'pages/CreateRole/index.ee';
|
||||
import EditRole from 'pages/EditRole/index.ee';
|
||||
import Authentication from 'pages/Authentication';
|
||||
import UserInterface from 'pages/UserInterface';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import Can from 'components/Can';
|
||||
import AdminApplications from 'pages/AdminApplications';
|
||||
import AdminApplication from 'pages/AdminApplication';
|
||||
|
||||
// TODO: consider introducing redirections to `/` as fallback
|
||||
export default (
|
||||
<>
|
||||
@@ -21,9 +19,7 @@ export default (
|
||||
path={URLS.USERS}
|
||||
element={
|
||||
<Can I="read" a="User">
|
||||
<AdminSettingsLayout>
|
||||
<Users />
|
||||
</AdminSettingsLayout>
|
||||
<Users />
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
@@ -32,9 +28,7 @@ export default (
|
||||
path={URLS.CREATE_USER}
|
||||
element={
|
||||
<Can I="create" a="User">
|
||||
<AdminSettingsLayout>
|
||||
<CreateUser />
|
||||
</AdminSettingsLayout>
|
||||
<CreateUser />
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
@@ -43,9 +37,7 @@ export default (
|
||||
path={URLS.USER_PATTERN}
|
||||
element={
|
||||
<Can I="update" a="User">
|
||||
<AdminSettingsLayout>
|
||||
<EditUser />
|
||||
</AdminSettingsLayout>
|
||||
<EditUser />
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
@@ -54,9 +46,7 @@ export default (
|
||||
path={URLS.ROLES}
|
||||
element={
|
||||
<Can I="read" a="Role">
|
||||
<AdminSettingsLayout>
|
||||
<Roles />
|
||||
</AdminSettingsLayout>
|
||||
<Roles />
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
@@ -65,9 +55,7 @@ export default (
|
||||
path={URLS.CREATE_ROLE}
|
||||
element={
|
||||
<Can I="create" a="Role">
|
||||
<AdminSettingsLayout>
|
||||
<CreateRole />
|
||||
</AdminSettingsLayout>
|
||||
<CreateRole />
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
@@ -76,9 +64,7 @@ export default (
|
||||
path={URLS.ROLE_PATTERN}
|
||||
element={
|
||||
<Can I="update" a="Role">
|
||||
<AdminSettingsLayout>
|
||||
<EditRole />
|
||||
</AdminSettingsLayout>
|
||||
<EditRole />
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
@@ -87,9 +73,7 @@ export default (
|
||||
path={URLS.USER_INTERFACE}
|
||||
element={
|
||||
<Can I="update" a="Config">
|
||||
<AdminSettingsLayout>
|
||||
<UserInterface />
|
||||
</AdminSettingsLayout>
|
||||
<UserInterface />
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
@@ -100,9 +84,7 @@ export default (
|
||||
<Can I="read" a="SamlAuthProvider">
|
||||
<Can I="update" a="SamlAuthProvider">
|
||||
<Can I="create" a="SamlAuthProvider">
|
||||
<AdminSettingsLayout>
|
||||
<Authentication />
|
||||
</AdminSettingsLayout>
|
||||
<Authentication />
|
||||
</Can>
|
||||
</Can>
|
||||
</Can>
|
||||
@@ -113,9 +95,7 @@ export default (
|
||||
path={URLS.ADMIN_APPS}
|
||||
element={
|
||||
<Can I="update" a="App">
|
||||
<AdminSettingsLayout>
|
||||
<AdminApplications />
|
||||
</AdminSettingsLayout>
|
||||
<AdminApplications />
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
@@ -124,9 +104,7 @@ export default (
|
||||
path={`${URLS.ADMIN_APP_PATTERN}/*`}
|
||||
element={
|
||||
<Can I="update" a="App">
|
||||
<AdminSettingsLayout>
|
||||
<AdminApplication />
|
||||
</AdminSettingsLayout>
|
||||
<AdminApplication />
|
||||
</Can>
|
||||
}
|
||||
/>
|
@@ -1,40 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
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 Can from 'components/Can';
|
||||
import apolloClient from 'graphql/client';
|
||||
import * as URLS from 'config/urls';
|
||||
import useAuthentication from 'hooks/useAuthentication';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
type AccountDropdownMenuProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
anchorEl: MenuProps['anchorEl'];
|
||||
id: string;
|
||||
};
|
||||
|
||||
function AccountDropdownMenu(
|
||||
props: AccountDropdownMenuProps
|
||||
): React.ReactElement {
|
||||
function AccountDropdownMenu(props) {
|
||||
const formatMessage = useFormatMessage();
|
||||
const authentication = useAuthentication();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { open, onClose, anchorEl, id } = props;
|
||||
|
||||
const logout = async () => {
|
||||
authentication.updateToken('');
|
||||
await apolloClient.clearStore();
|
||||
|
||||
onClose();
|
||||
|
||||
navigate(URLS.LOGIN);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
@@ -51,7 +36,7 @@ function AccountDropdownMenu(
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
<MenuItem component={Link} to={URLS.SETTINGS_DASHBOARD}>
|
||||
<MenuItem component={Link} to={URLS.SETTINGS_DASHBOARD} onClick={onClose}>
|
||||
{formatMessage('accountDropdownMenu.settings')}
|
||||
</MenuItem>
|
||||
|
||||
@@ -59,6 +44,7 @@ function AccountDropdownMenu(
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={URLS.ADMIN_SETTINGS_DASHBOARD}
|
||||
onClick={onClose}
|
||||
>
|
||||
{formatMessage('accountDropdownMenu.adminSettings')}
|
||||
</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;
|
@@ -1,4 +1,4 @@
|
||||
import type { IApp, IField, IJSONObject } from 'types';
|
||||
import PropTypes from 'prop-types';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
@@ -6,32 +6,24 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import * as React from 'react';
|
||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { AppPropType } from 'propTypes/propTypes';
|
||||
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||
import InputCreator from 'components/InputCreator';
|
||||
import * as URLS from 'config/urls';
|
||||
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { generateExternalLink } from '../../helpers/translationValues';
|
||||
import { generateExternalLink } from 'helpers/translationValues';
|
||||
import { Form } from './style';
|
||||
|
||||
type AddAppConnectionProps = {
|
||||
onClose: (response: Record<string, unknown>) => void;
|
||||
application: IApp;
|
||||
connectionId?: string;
|
||||
};
|
||||
|
||||
export default function AddAppConnection(
|
||||
props: AddAppConnectionProps
|
||||
): React.ReactElement {
|
||||
function AddAppConnection(props) {
|
||||
const { application, connectionId, onClose } = props;
|
||||
const { name, authDocUrl, key, auth } = application;
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const formatMessage = useFormatMessage();
|
||||
const [error, setError] = React.useState<IJSONObject | null>(null);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [inProgress, setInProgress] = React.useState(false);
|
||||
const hasConnection = Boolean(connectionId);
|
||||
const useShared = searchParams.get('shared') === 'true';
|
||||
@@ -42,7 +34,6 @@ export default function AddAppConnection(
|
||||
appAuthClientId,
|
||||
useShared: !!appAuthClientId,
|
||||
});
|
||||
|
||||
React.useEffect(function relayProviderData() {
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({
|
||||
@@ -52,51 +43,41 @@ export default function AddAppConnection(
|
||||
window.close();
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(
|
||||
function initiateSharedAuthenticationForGivenAuthClient() {
|
||||
if (!appAuthClientId) return;
|
||||
if (!authenticate) return;
|
||||
|
||||
const asyncAuthenticate = async () => {
|
||||
await authenticate();
|
||||
|
||||
navigate(URLS.APP_CONNECTIONS(key));
|
||||
};
|
||||
|
||||
asyncAuthenticate();
|
||||
},
|
||||
[appAuthClientId, authenticate]
|
||||
[appAuthClientId, authenticate],
|
||||
);
|
||||
|
||||
const handleClientClick = (appAuthClientId: string) =>
|
||||
const handleClientClick = (appAuthClientId) =>
|
||||
navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId));
|
||||
|
||||
const handleAuthClientsDialogClose = () =>
|
||||
navigate(URLS.APP_CONNECTIONS(key));
|
||||
|
||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
||||
const submitHandler = React.useCallback(
|
||||
async (data) => {
|
||||
if (!authenticate) return;
|
||||
|
||||
setInProgress(true);
|
||||
|
||||
try {
|
||||
const response = await authenticate({
|
||||
fields: data,
|
||||
});
|
||||
onClose(response as Record<string, unknown>);
|
||||
onClose(response);
|
||||
} catch (err) {
|
||||
const error = err as IJSONObject;
|
||||
const error = err;
|
||||
console.log(error);
|
||||
setError((error.graphQLErrors as IJSONObject[])?.[0]);
|
||||
setError(error.graphQLErrors?.[0]);
|
||||
} finally {
|
||||
setInProgress(false);
|
||||
}
|
||||
},
|
||||
[authenticate]
|
||||
[authenticate],
|
||||
);
|
||||
|
||||
if (useShared)
|
||||
return (
|
||||
<AppAuthClientsDialog
|
||||
@@ -105,9 +86,7 @@ export default function AddAppConnection(
|
||||
onClientClick={handleClientClick}
|
||||
/>
|
||||
);
|
||||
|
||||
if (appAuthClientId) return <React.Fragment />;
|
||||
|
||||
return (
|
||||
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
|
||||
<DialogTitle>
|
||||
@@ -142,7 +121,7 @@ export default function AddAppConnection(
|
||||
<DialogContent>
|
||||
<DialogContentText tabIndex={-1} component="div">
|
||||
<Form onSubmit={submitHandler}>
|
||||
{auth?.fields?.map((field: IField) => (
|
||||
{auth?.fields?.map((field) => (
|
||||
<InputCreator key={field.key} schema={field} />
|
||||
))}
|
||||
|
||||
@@ -162,3 +141,11 @@ export default function AddAppConnection(
|
||||
</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 BaseForm from 'components/Form';
|
||||
|
||||
export const Form = styled(BaseForm)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { Link } from 'react-router-dom';
|
||||
import debounce from 'lodash/debounce';
|
||||
@@ -19,67 +20,53 @@ import InputLabel from '@mui/material/InputLabel';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import Box from '@mui/material/Box';
|
||||
import type { IApp } from 'types';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import AppIcon from 'components/AppIcon';
|
||||
import { GET_APPS } from 'graphql/queries/get-apps';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
function createConnectionOrFlow(appKey: string, supportsConnections = false) {
|
||||
function createConnectionOrFlow(appKey, supportsConnections = false) {
|
||||
if (!supportsConnections) {
|
||||
return URLS.CREATE_FLOW_WITH_APP(appKey);
|
||||
}
|
||||
|
||||
return URLS.APP_ADD_CONNECTION(appKey);
|
||||
}
|
||||
|
||||
type AddNewAppConnectionProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function AddNewAppConnection(
|
||||
props: AddNewAppConnectionProps
|
||||
): React.ReactElement {
|
||||
function AddNewAppConnection(props) {
|
||||
const { onClose } = props;
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const formatMessage = useFormatMessage();
|
||||
const [appName, setAppName] = React.useState<string | null>(null);
|
||||
const [appName, setAppName] = React.useState(null);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [getApps, { data }] = useLazyQuery(GET_APPS, {
|
||||
onCompleted: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
const fetchData = React.useMemo(
|
||||
() => debounce((name) => getApps({ variables: { name } }), 300),
|
||||
[getApps]
|
||||
[getApps],
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
function fetchAppsOnAppNameChange() {
|
||||
setLoading(true);
|
||||
|
||||
fetchData(appName);
|
||||
},
|
||||
[fetchData, appName]
|
||||
[fetchData, appName],
|
||||
);
|
||||
|
||||
React.useEffect(function cancelDebounceOnUnmount() {
|
||||
return () => {
|
||||
fetchData.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={true}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
data-test="add-app-connection-dialog">
|
||||
data-test="add-app-connection-dialog"
|
||||
>
|
||||
<DialogTitle>{formatMessage('apps.addNewAppConnection')}</DialogTitle>
|
||||
|
||||
<Box px={3}>
|
||||
@@ -123,7 +110,7 @@ export default function AddNewAppConnection(
|
||||
)}
|
||||
|
||||
{!loading &&
|
||||
data?.getApps?.map((app: IApp) => (
|
||||
data?.getApps?.map((app) => (
|
||||
<ListItem disablePadding key={app.name} data-test="app-list-item">
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
@@ -151,3 +138,9 @@ export default function AddNewAppConnection(
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
AddNewAppConnection.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AddNewAppConnection;
|
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import type { IField } from 'types';
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
@@ -7,32 +7,16 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||
import type { UseFormProps } from 'react-hook-form';
|
||||
import type { ApolloError } from '@apollo/client';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
|
||||
import { FieldPropType } from 'propTypes/propTypes';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import InputCreator from 'components/InputCreator';
|
||||
import Switch from 'components/Switch';
|
||||
import TextField from 'components/TextField';
|
||||
|
||||
import { Form } from './style';
|
||||
|
||||
type AdminApplicationAuthClientDialogProps = {
|
||||
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 {
|
||||
function AdminApplicationAuthClientDialog(props) {
|
||||
const {
|
||||
error,
|
||||
onClose,
|
||||
@@ -45,7 +29,6 @@ export default function AdminApplicationAuthClientDialog(
|
||||
disabled = false,
|
||||
} = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
return (
|
||||
<Dialog open={true} onClose={onClose}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
@@ -80,7 +63,7 @@ export default function AdminApplicationAuthClientDialog(
|
||||
label={formatMessage('authClient.inputName')}
|
||||
fullWidth
|
||||
/>
|
||||
{authFields?.map((field: IField) => (
|
||||
{authFields?.map((field) => (
|
||||
<InputCreator key={field.key} schema={field} />
|
||||
))}
|
||||
<LoadingButton
|
||||
@@ -102,3 +85,17 @@ export default function AdminApplicationAuthClientDialog(
|
||||
</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 BaseForm from 'components/Form';
|
||||
|
||||
export const Form = styled(BaseForm)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
@@ -7,27 +8,17 @@ import CardContent from '@mui/material/CardContent';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
||||
|
||||
import NoResultFound from 'components/NoResultFound';
|
||||
|
||||
type AdminApplicationAuthClientsProps = {
|
||||
appKey: string;
|
||||
};
|
||||
|
||||
function AdminApplicationAuthClients(
|
||||
props: AdminApplicationAuthClientsProps
|
||||
): React.ReactElement {
|
||||
function AdminApplicationAuthClients(props) {
|
||||
const { appKey } = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
const { appAuthClients, loading } = useAppAuthClients({ appKey });
|
||||
|
||||
if (loading)
|
||||
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
|
||||
|
||||
if (!appAuthClients?.length) {
|
||||
return (
|
||||
<NoResultFound
|
||||
@@ -36,7 +27,6 @@ function AdminApplicationAuthClients(
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sortedAuthClients = appAuthClients.slice().sort((a, b) => {
|
||||
if (a.id < b.id) {
|
||||
return -1;
|
||||
@@ -46,7 +36,6 @@ function AdminApplicationAuthClients(
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{sortedAuthClients.map((client) => (
|
||||
@@ -67,7 +56,7 @@ function AdminApplicationAuthClients(
|
||||
label={formatMessage(
|
||||
client?.active
|
||||
? 'adminAppsAuthClients.statusActive'
|
||||
: 'adminAppsAuthClients.statusInactive'
|
||||
: 'adminAppsAuthClients.statusInactive',
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -86,4 +75,8 @@ function AdminApplicationAuthClients(
|
||||
);
|
||||
}
|
||||
|
||||
AdminApplicationAuthClients.propTypes = {
|
||||
appKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AdminApplicationAuthClients;
|
@@ -1,24 +1,15 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { IApp } from 'types';
|
||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||
import { useMutation } from '@apollo/client';
|
||||
|
||||
import { AppPropType } from 'propTypes/propTypes';
|
||||
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
||||
import { CREATE_APP_AUTH_CLIENT } from 'graphql/mutations/create-app-auth-client';
|
||||
|
||||
import useAppConfig from 'hooks/useAppConfig.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
||||
|
||||
type AdminApplicationCreateAuthClientProps = {
|
||||
appKey: string;
|
||||
application: IApp;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function AdminApplicationCreateAuthClient(
|
||||
props: AdminApplicationCreateAuthClientProps
|
||||
): React.ReactElement {
|
||||
function AdminApplicationCreateAuthClient(props) {
|
||||
const { appKey, application, onClose } = props;
|
||||
const { auth } = application;
|
||||
const formatMessage = useFormatMessage();
|
||||
@@ -37,10 +28,8 @@ export default function AdminApplicationCreateAuthClient(
|
||||
refetchQueries: ['GetAppAuthClients'],
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
|
||||
const submitHandler: SubmitHandler<FieldValues> = async (values) => {
|
||||
const submitHandler = async (values) => {
|
||||
let appConfigId = appConfig?.id;
|
||||
|
||||
if (!appConfigId) {
|
||||
const { data: appConfigData } = await createAppConfig({
|
||||
variables: {
|
||||
@@ -54,9 +43,7 @@ export default function AdminApplicationCreateAuthClient(
|
||||
});
|
||||
appConfigId = appConfigData.createAppConfig.id;
|
||||
}
|
||||
|
||||
const { name, active, ...formattedAuthDefaults } = values;
|
||||
|
||||
await createAppAuthClient({
|
||||
variables: {
|
||||
input: {
|
||||
@@ -67,17 +54,13 @@ export default function AdminApplicationCreateAuthClient(
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const getAuthFieldsDefaultValues = useCallback(() => {
|
||||
if (!auth?.fields) {
|
||||
return {};
|
||||
}
|
||||
const defaultValues: {
|
||||
[key: string]: any;
|
||||
} = {};
|
||||
const defaultValues = {};
|
||||
auth.fields.forEach((field) => {
|
||||
if (field.value || field.type !== 'string') {
|
||||
defaultValues[field.key] = field.value;
|
||||
@@ -87,16 +70,14 @@ export default function AdminApplicationCreateAuthClient(
|
||||
});
|
||||
return defaultValues;
|
||||
}, [auth?.fields]);
|
||||
|
||||
const defaultValues = useMemo(
|
||||
() => ({
|
||||
name: '',
|
||||
active: false,
|
||||
...getAuthFieldsDefaultValues(),
|
||||
}),
|
||||
[getAuthFieldsDefaultValues]
|
||||
[getAuthFieldsDefaultValues],
|
||||
);
|
||||
|
||||
return (
|
||||
<AdminApplicationAuthClientDialog
|
||||
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 useAppConfig from 'hooks/useAppConfig.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
@@ -6,39 +7,29 @@ import Paper from '@mui/material/Paper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import { useMutation } from '@apollo/client';
|
||||
|
||||
import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config';
|
||||
import { UPDATE_APP_CONFIG } from 'graphql/mutations/update-app-config';
|
||||
|
||||
import Form from 'components/Form';
|
||||
import { Switch } from './style';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
|
||||
type AdminApplicationSettingsProps = {
|
||||
appKey: string;
|
||||
};
|
||||
|
||||
function AdminApplicationSettings(
|
||||
props: AdminApplicationSettingsProps
|
||||
): React.ReactElement {
|
||||
function AdminApplicationSettings(props) {
|
||||
const { appConfig, loading } = useAppConfig(props.appKey);
|
||||
const [createAppConfig, { loading: loadingCreateAppConfig }] = useMutation(
|
||||
CREATE_APP_CONFIG,
|
||||
{
|
||||
refetchQueries: ['GetAppConfig'],
|
||||
}
|
||||
},
|
||||
);
|
||||
const [updateAppConfig, { loading: loadingUpdateAppConfig }] = useMutation(
|
||||
UPDATE_APP_CONFIG,
|
||||
{
|
||||
refetchQueries: ['GetAppConfig'],
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
const handleSubmit = async (values) => {
|
||||
try {
|
||||
if (!appConfig) {
|
||||
await createAppConfig({
|
||||
@@ -56,23 +47,21 @@ function AdminApplicationSettings(
|
||||
enqueueSnackbar(formatMessage('adminAppsSettings.successfullySaved'), {
|
||||
variant: 'success',
|
||||
SnackbarProps: {
|
||||
'data-test': 'snackbar-save-admin-apps-settings-success'
|
||||
}
|
||||
'data-test': 'snackbar-save-admin-apps-settings-success',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error('Failed while saving!');
|
||||
}
|
||||
};
|
||||
|
||||
const defaultValues = useMemo(
|
||||
() => ({
|
||||
allowCustomConnection: appConfig?.allowCustomConnection || false,
|
||||
shared: appConfig?.shared || false,
|
||||
disabled: appConfig?.disabled || false,
|
||||
}),
|
||||
[appConfig]
|
||||
[appConfig],
|
||||
);
|
||||
|
||||
return (
|
||||
<Form
|
||||
defaultValues={defaultValues}
|
||||
@@ -123,4 +112,8 @@ function AdminApplicationSettings(
|
||||
);
|
||||
}
|
||||
|
||||
AdminApplicationSettings.propTypes = {
|
||||
appKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AdminApplicationSettings;
|
@@ -1,6 +1,5 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import SwitchBase from 'components/Switch';
|
||||
|
||||
export const Switch = styled(SwitchBase)`
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
@@ -1,55 +1,52 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import type { IApp } from 'types';
|
||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||
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 AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog';
|
||||
import useAdminAppAuthClient from 'hooks/useAdminAppAuthClient.ee';
|
||||
|
||||
type AdminApplicationUpdateAuthClientProps = {
|
||||
application: IApp;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function AdminApplicationUpdateAuthClient(
|
||||
props: AdminApplicationUpdateAuthClientProps
|
||||
): React.ReactElement {
|
||||
function AdminApplicationUpdateAuthClient(props) {
|
||||
const { application, onClose } = props;
|
||||
const { auth } = application;
|
||||
const authFields = auth?.fields?.map((field) => ({
|
||||
...field,
|
||||
required: false,
|
||||
}));
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const { clientId } = useParams();
|
||||
const { appAuthClient, loading: loadingAuthClient } =
|
||||
useAppAuthClient(clientId);
|
||||
|
||||
const { data: adminAppAuthClient, isLoading: isAdminAuthClientLoading } =
|
||||
useAdminAppAuthClient(clientId);
|
||||
|
||||
const [updateAppAuthClient, { loading: loadingUpdateAppAuthClient, error }] =
|
||||
useMutation(UPDATE_APP_AUTH_CLIENT, {
|
||||
refetchQueries: ['GetAppAuthClients'],
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
|
||||
const submitHandler: SubmitHandler<FieldValues> = async (values) => {
|
||||
if (!appAuthClient) {
|
||||
const authFields = auth?.fields?.map((field) => ({
|
||||
...field,
|
||||
required: false,
|
||||
}));
|
||||
|
||||
const submitHandler = async (values) => {
|
||||
if (!adminAppAuthClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, active, ...formattedAuthDefaults } = values;
|
||||
|
||||
await updateAppAuthClient({
|
||||
variables: {
|
||||
input: {
|
||||
id: appAuthClient.id,
|
||||
id: adminAppAuthClient.data.id,
|
||||
name,
|
||||
active,
|
||||
formattedAuthDefaults,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -57,9 +54,8 @@ export default function AdminApplicationUpdateAuthClient(
|
||||
if (!authFields) {
|
||||
return {};
|
||||
}
|
||||
const defaultValues: {
|
||||
[key: string]: any;
|
||||
} = {};
|
||||
|
||||
const defaultValues = {};
|
||||
authFields.forEach((field) => {
|
||||
if (field.value || field.type !== 'string') {
|
||||
defaultValues[field.key] = field.value;
|
||||
@@ -72,11 +68,11 @@ export default function AdminApplicationUpdateAuthClient(
|
||||
|
||||
const defaultValues = useMemo(
|
||||
() => ({
|
||||
name: appAuthClient?.name || '',
|
||||
active: appAuthClient?.active || false,
|
||||
name: adminAppAuthClient?.data?.name || '',
|
||||
active: adminAppAuthClient?.data?.active || false,
|
||||
...getAuthFieldsDefaultValues(),
|
||||
}),
|
||||
[appAuthClient, getAuthFieldsDefaultValues]
|
||||
[adminAppAuthClient, getAuthFieldsDefaultValues],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -84,12 +80,19 @@ export default function AdminApplicationUpdateAuthClient(
|
||||
onClose={onClose}
|
||||
error={error}
|
||||
title={formatMessage('updateAuthClient.title')}
|
||||
loading={loadingAuthClient}
|
||||
loading={isAdminAuthClientLoading}
|
||||
submitHandler={submitHandler}
|
||||
authFields={authFields}
|
||||
submitting={loadingUpdateAppAuthClient}
|
||||
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 BrushIcon from '@mui/icons-material/Brush';
|
||||
import AppsIcon from '@mui/icons-material/Apps';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIconComponent } from '@mui/icons-material';
|
||||
import AppBar from 'components/AppBar';
|
||||
import Drawer from 'components/Drawer';
|
||||
import Can from 'components/Can';
|
||||
import * as URLS from 'config/urls';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||
|
||||
type SettingsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
type DrawerLink = {
|
||||
Icon: SvgIconComponent;
|
||||
primary: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
function createDrawerLinks({
|
||||
canReadRole,
|
||||
canReadUser,
|
||||
canUpdateConfig,
|
||||
canManageSamlAuthProvider,
|
||||
canUpdateApp,
|
||||
}: {
|
||||
canReadRole: boolean;
|
||||
canReadUser: boolean;
|
||||
canUpdateConfig: boolean;
|
||||
canManageSamlAuthProvider: boolean;
|
||||
canUpdateApp: boolean;
|
||||
}) {
|
||||
const items = [
|
||||
canReadUser
|
||||
@@ -82,20 +66,16 @@ function createDrawerLinks({
|
||||
dataTest: 'apps-drawer-link',
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean) as DrawerLink[];
|
||||
|
||||
].filter(Boolean);
|
||||
return items;
|
||||
}
|
||||
|
||||
export default function SettingsLayout({
|
||||
children,
|
||||
}: SettingsLayoutProps): React.ReactElement {
|
||||
function SettingsLayout() {
|
||||
const theme = useTheme();
|
||||
const formatMessage = useFormatMessage();
|
||||
const currentUserAbility = useCurrentUserAbility();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||
|
||||
const openDrawer = () => setDrawerOpen(true);
|
||||
const closeDrawer = () => setDrawerOpen(false);
|
||||
const drawerLinks = createDrawerLinks({
|
||||
@@ -108,7 +88,6 @@ export default function SettingsLayout({
|
||||
currentUserAbility.can('create', 'SamlAuthProvider'),
|
||||
canUpdateApp: currentUserAbility.can('update', 'App'),
|
||||
});
|
||||
|
||||
const drawerBottomLinks = [
|
||||
{
|
||||
Icon: ArrowBackIosNewIcon,
|
||||
@@ -117,15 +96,13 @@ export default function SettingsLayout({
|
||||
dataTest: 'go-back-drawer-link',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Can I="read" a="User">
|
||||
<AppBar
|
||||
drawerOpen={isDrawerOpen}
|
||||
onDrawerOpen={openDrawer}
|
||||
onDrawerClose={closeDrawer}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Drawer
|
||||
links={drawerLinks}
|
||||
@@ -134,13 +111,13 @@ export default function SettingsLayout({
|
||||
onOpen={openDrawer}
|
||||
onClose={closeDrawer}
|
||||
/>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Toolbar />
|
||||
|
||||
{children}
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
</Can>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsLayout;
|
@@ -5,11 +5,7 @@ import { mutateAndGetClient } from 'graphql/client';
|
||||
import useAuthentication from 'hooks/useAuthentication';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
|
||||
type ApolloProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
|
||||
const ApolloProvider = (props) => {
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const authentication = useAuthentication();
|
||||
|
||||
@@ -18,11 +14,11 @@ const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
|
||||
enqueueSnackbar(message, {
|
||||
variant: 'error',
|
||||
SnackbarProps: {
|
||||
'data-test': 'snackbar-error'
|
||||
}
|
||||
'data-test': 'snackbar-error',
|
||||
},
|
||||
});
|
||||
},
|
||||
[enqueueSnackbar]
|
||||
[enqueueSnackbar],
|
||||
);
|
||||
|
||||
const client = React.useMemo(() => {
|
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import List from '@mui/material/List';
|
||||
@@ -5,33 +6,23 @@ import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import * as React from 'react';
|
||||
|
||||
import useAppAuthClients from 'hooks/useAppAuthClients.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
type AppAuthClientsDialogProps = {
|
||||
appKey: string;
|
||||
onClientClick: (appAuthClientId: string) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function AppAuthClientsDialog(props: AppAuthClientsDialogProps) {
|
||||
function AppAuthClientsDialog(props) {
|
||||
const { appKey, onClientClick, onClose } = props;
|
||||
const { appAuthClients } = useAppAuthClients({ appKey, active: true });
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
React.useEffect(
|
||||
function autoAuthenticateSingleClient() {
|
||||
if (appAuthClients?.length === 1) {
|
||||
onClientClick(appAuthClients[0].id);
|
||||
}
|
||||
},
|
||||
[appAuthClients]
|
||||
[appAuthClients],
|
||||
);
|
||||
|
||||
if (!appAuthClients?.length || appAuthClients?.length === 1)
|
||||
return <React.Fragment />;
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} open={true}>
|
||||
<DialogTitle>{formatMessage('appAuthClientsDialog.title')}</DialogTitle>
|
||||
@@ -48,3 +39,11 @@ export default function AppAuthClientsDialog(props: AppAuthClientsDialogProps) {
|
||||
</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 MenuIcon from '@mui/icons-material/Menu';
|
||||
import MenuOpenIcon from '@mui/icons-material/MenuOpen';
|
||||
import MuiAppBar from '@mui/material/AppBar';
|
||||
import type { ContainerProps } from '@mui/material/Container';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import * as React from 'react';
|
||||
|
||||
import AccountDropdownMenu from 'components/AccountDropdownMenu';
|
||||
import Container from 'components/Container';
|
||||
import Logo from 'components/Logo/index';
|
||||
import TrialStatusBadge from 'components/TrialStatusBadge/index.ee';
|
||||
import * as URLS from 'config/urls';
|
||||
|
||||
import { Link } from './style';
|
||||
|
||||
type AppBarProps = {
|
||||
drawerOpen: boolean;
|
||||
onDrawerOpen: () => void;
|
||||
onDrawerClose: () => void;
|
||||
maxWidth?: ContainerProps['maxWidth'];
|
||||
};
|
||||
|
||||
const accountMenuId = 'account-menu';
|
||||
|
||||
export default function AppBar(props: AppBarProps): React.ReactElement {
|
||||
function AppBar(props) {
|
||||
const { drawerOpen, onDrawerOpen, onDrawerClose, maxWidth = false } = props;
|
||||
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const [accountMenuAnchorElement, setAccountMenuAnchorElement] =
|
||||
React.useState<null | HTMLElement>(null);
|
||||
|
||||
React.useState(null);
|
||||
const isMenuOpen = Boolean(accountMenuAnchorElement);
|
||||
|
||||
const handleAccountMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
const handleAccountMenuOpen = (event) => {
|
||||
setAccountMenuAnchorElement(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleAccountMenuClose = () => {
|
||||
setAccountMenuAnchorElement(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<MuiAppBar data-test="app-bar">
|
||||
<Container maxWidth={maxWidth} disableGutters>
|
||||
@@ -91,3 +76,15 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
|
||||
</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 { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
export const Link = styled(RouterLink)(() => ({
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
@@ -1,29 +1,14 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import type { PopoverProps } from '@mui/material/Popover';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import type { IConnection } from 'types';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { ConnectionPropType } from 'propTypes/propTypes';
|
||||
|
||||
type Action = {
|
||||
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 {
|
||||
function ContextMenu(props) {
|
||||
const {
|
||||
appKey,
|
||||
connection,
|
||||
@@ -33,18 +18,15 @@ export default function ContextMenu(
|
||||
disableReconnection,
|
||||
} = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const createActionHandler = React.useCallback(
|
||||
(action: Action) => {
|
||||
return function clickHandler(event: React.MouseEvent) {
|
||||
(action) => {
|
||||
return function clickHandler(event) {
|
||||
onMenuItemClick(event, action);
|
||||
|
||||
onClose();
|
||||
};
|
||||
},
|
||||
[onMenuItemClick, onClose]
|
||||
[onMenuItemClick, onClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={true}
|
||||
@@ -70,7 +52,7 @@ export default function ContextMenu(
|
||||
to={URLS.APP_RECONNECT_CONNECTION(
|
||||
appKey,
|
||||
connection.id,
|
||||
connection.appAuthClientId
|
||||
connection.appAuthClientId,
|
||||
)}
|
||||
onClick={createActionHandler({ type: 'reconnect' })}
|
||||
>
|
||||
@@ -83,3 +65,17 @@ export default function ContextMenu(
|
||||
</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 { DateTime } from 'luxon';
|
||||
import * as React from 'react';
|
||||
|
||||
import type { IConnection } from 'types';
|
||||
import ConnectionContextMenu from 'components/AppConnectionContextMenu';
|
||||
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
|
||||
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { ConnectionPropType } from 'propTypes/propTypes';
|
||||
import { CardContent, Typography } from './style';
|
||||
|
||||
type AppConnectionRowProps = {
|
||||
connection: IConnection;
|
||||
};
|
||||
|
||||
const countTranslation = (value: React.ReactNode) => (
|
||||
const countTranslation = (value) => (
|
||||
<>
|
||||
<Typography variant="body1">{value}</Typography>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
|
||||
function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
function AppConnectionRow(props) {
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const [verificationVisible, setVerificationVisible] = React.useState(false);
|
||||
const [testConnection, { called: testCalled, loading: testLoading }] =
|
||||
@@ -43,7 +37,6 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
},
|
||||
});
|
||||
const [deleteConnection] = useMutation(DELETE_CONNECTION);
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
const {
|
||||
id,
|
||||
@@ -54,17 +47,14 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
flowCount,
|
||||
reconnectable,
|
||||
} = props.connection;
|
||||
|
||||
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
|
||||
|
||||
const contextButtonRef = React.useRef(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
|
||||
const onContextMenuAction = React.useCallback(
|
||||
async (event, action: { [key: string]: string }) => {
|
||||
async (event, action) => {
|
||||
if (action.type === 'delete') {
|
||||
await deleteConnection({
|
||||
variables: { input: { id } },
|
||||
@@ -73,13 +63,11 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
__typename: 'Connection',
|
||||
id,
|
||||
});
|
||||
|
||||
cache.evict({
|
||||
id: connectionCacheId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
|
||||
variant: 'success',
|
||||
SnackbarProps: {
|
||||
@@ -91,13 +79,11 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
testConnection({ variables: { id } });
|
||||
}
|
||||
},
|
||||
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]
|
||||
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar],
|
||||
);
|
||||
|
||||
const relativeCreatedAt = DateTime.fromMillis(
|
||||
parseInt(createdAt, 10)
|
||||
parseInt(createdAt, 10),
|
||||
).toRelative();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card sx={{ my: 2 }} data-test="app-connection-row">
|
||||
@@ -125,14 +111,17 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{verificationVisible && testCalled && !testLoading && verified && (
|
||||
<>
|
||||
<CheckCircleIcon fontSize="small" color="success" />
|
||||
<Typography variant="caption">
|
||||
{formatMessage('connection.testSuccessful')}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{verificationVisible &&
|
||||
testCalled &&
|
||||
!testLoading &&
|
||||
verified && (
|
||||
<>
|
||||
<CheckCircleIcon fontSize="small" color="success" />
|
||||
<Typography variant="caption">
|
||||
{formatMessage('connection.testSuccessful')}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{verificationVisible &&
|
||||
testCalled &&
|
||||
!testLoading &&
|
||||
@@ -180,4 +169,8 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
AppConnectionRow.propTypes = {
|
||||
connection: ConnectionPropType.isRequired,
|
||||
};
|
||||
|
||||
export default AppConnectionRow;
|
@@ -1,7 +1,6 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import MuiCardContent from '@mui/material/CardContent';
|
||||
import MuiTypography from '@mui/material/Typography';
|
||||
|
||||
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto',
|
||||
@@ -9,7 +8,6 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||
gridColumnGap: theme.spacing(2),
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
export const Typography = styled(MuiTypography)(() => ({
|
||||
textAlign: 'center',
|
||||
display: 'inline-block',
|
@@ -1,29 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
import type { IConnection } from 'types';
|
||||
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
||||
import AppConnectionRow from 'components/AppConnectionRow';
|
||||
import NoResultFound from 'components/NoResultFound';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import * as URLS from 'config/urls';
|
||||
|
||||
type AppConnectionsProps = {
|
||||
appKey: string;
|
||||
};
|
||||
|
||||
export default function AppConnections(
|
||||
props: AppConnectionsProps
|
||||
): React.ReactElement {
|
||||
function AppConnections(props) {
|
||||
const { appKey } = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
const { data } = useQuery(GET_APP_CONNECTIONS, {
|
||||
variables: { key: appKey },
|
||||
});
|
||||
const appConnections: IConnection[] = data?.getApp?.connections || [];
|
||||
|
||||
const appConnections = data?.getApp?.connections || [];
|
||||
const hasConnections = appConnections?.length;
|
||||
|
||||
if (!hasConnections) {
|
||||
return (
|
||||
<NoResultFound
|
||||
@@ -36,9 +27,15 @@ export default function AppConnections(
|
||||
|
||||
return (
|
||||
<>
|
||||
{appConnections.map((appConnection: IConnection) => (
|
||||
{appConnections.map((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 { Link, useSearchParams } from 'react-router-dom';
|
||||
import { GET_FLOWS } from 'graphql/queries/get-flows';
|
||||
import Pagination from '@mui/material/Pagination';
|
||||
import PaginationItem from '@mui/material/PaginationItem';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import AppFlowRow from 'components/FlowRow';
|
||||
import NoResultFound from 'components/NoResultFound';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import type { IFlow } from 'types';
|
||||
|
||||
type AppFlowsProps = {
|
||||
appKey: string;
|
||||
};
|
||||
|
||||
const FLOW_PER_PAGE = 10;
|
||||
|
||||
const getLimitAndOffset = (page: number) => ({
|
||||
const getLimitAndOffset = (page) => ({
|
||||
limit: FLOW_PER_PAGE,
|
||||
offset: (page - 1) * FLOW_PER_PAGE,
|
||||
});
|
||||
|
||||
export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
||||
function AppFlows(props) {
|
||||
const { appKey } = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -36,10 +28,8 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
||||
});
|
||||
const getFlows = data?.getFlows || {};
|
||||
const { pageInfo, edges } = getFlows;
|
||||
|
||||
const flows: IFlow[] = edges?.map(({ node }: { node: IFlow }) => node);
|
||||
const flows = edges?.map(({ node }) => node);
|
||||
const hasFlows = flows?.length;
|
||||
|
||||
if (!hasFlows) {
|
||||
return (
|
||||
<NoResultFound
|
||||
@@ -49,10 +39,9 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{flows?.map((appFlow: IFlow) => (
|
||||
{flows?.map((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 PropTypes from 'prop-types';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import type { AvatarProps } from '@mui/material/Avatar';
|
||||
|
||||
type AppIconProps = {
|
||||
name?: string;
|
||||
url?: string;
|
||||
color?: string;
|
||||
variant?: AvatarProps['variant'];
|
||||
};
|
||||
|
||||
const inlineImgStyle: React.CSSProperties = {
|
||||
const inlineImgStyle = {
|
||||
objectFit: 'contain',
|
||||
};
|
||||
|
||||
export default function AppIcon(
|
||||
props: AppIconProps & AvatarProps
|
||||
): React.ReactElement {
|
||||
function AppIcon(props) {
|
||||
const { name, url, color, sx = {}, variant = 'square', ...restProps } = props;
|
||||
|
||||
const initialLetter = name?.[0];
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
component="span"
|
||||
@@ -28,8 +15,22 @@ export default function AppIcon(
|
||||
imgProps={{ style: inlineImgStyle }}
|
||||
src={url}
|
||||
alt={name}
|
||||
children={initialLetter}
|
||||
{...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