Compare commits
34 Commits
test-docs-
...
AUT-860
Author | SHA1 | Date | |
---|---|---|---|
![]() |
aee0674d20 | ||
![]() |
fc4eeed764 | ||
![]() |
3596d13be1 | ||
![]() |
104d49ea1c | ||
![]() |
7057317446 | ||
![]() |
280575df88 | ||
![]() |
d2cb434b7b | ||
![]() |
2ecb802a2e | ||
![]() |
46e706c415 | ||
![]() |
3a57349d8a | ||
![]() |
754c3269ec | ||
![]() |
a079842408 | ||
![]() |
7664b58553 | ||
![]() |
de77488f7e | ||
![]() |
d808afd21b | ||
![]() |
b68aff76a1 | ||
![]() |
6da7fe158f | ||
![]() |
4dbc7fdc7d | ||
![]() |
ad1e1f7eca | ||
![]() |
9c3f7a3823 | ||
![]() |
86f4cb7701 | ||
![]() |
359a90245d | ||
![]() |
d8d7d86359 | ||
![]() |
7189b629c0 | ||
![]() |
55c9b5566c | ||
![]() |
ab671ccbf7 | ||
![]() |
316bda8c3f | ||
![]() |
76f77e8a4c | ||
![]() |
4a99d5eab7 | ||
![]() |
473d287c6d | ||
![]() |
bddd9896e4 | ||
![]() |
95eb115965 | ||
![]() |
ec87c7f21c | ||
![]() |
5c684cd499 |
2
.github/workflows/docs-change.yml
vendored
2
.github/workflows/docs-change.yml
vendored
@@ -13,8 +13,6 @@ jobs:
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const github = require('@actions/github');
|
||||
const { context } = github;
|
||||
const { pull_request } = context.payload;
|
||||
|
||||
const label = 'documentation-change';
|
||||
|
@@ -31,6 +31,7 @@
|
||||
"accounting": "^0.4.1",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"axios": "1.6.0",
|
||||
"basic-ftp": "^5.0.5",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bullmq": "^3.0.0",
|
||||
"cors": "^2.8.5",
|
||||
|
1
packages/backend/src/apps/ftp/assets/favicon.svg
Normal file
1
packages/backend/src/apps/ftp/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 109.36" style="enable-background:new 0 0 122.88 109.36" xml:space="preserve"><g><path d="M14.69,16.44h3.56V8.63c0-1.09,0.88-1.97,1.97-1.97h3.5V1.97C23.72,0.88,24.6,0,25.69,0h31.92c1.09,0,1.97,0.88,1.97,1.97 v4.69h43.05c1.09,0,1.97,0.88,1.97,1.97v7.82h3.59c1.09,0,1.97,0.88,1.97,1.97c0,0.14-0.01,0.28-0.04,0.42l-6.88,50.59 c-0.22,1.65-0.95,3.18-2.05,4.29c-1.1,1.1-2.54,1.78-4.25,1.78H70.95v5.86h2.12c1.54,0,2.8,1.26,2.8,2.8v5.06h10.44 c1.25,0,2.32,0.83,2.67,1.97h26.1c3.73,0,6.14,2.07,7.2,4.71c0.4,1,0.6,2.08,0.6,3.14c0,1.06-0.2,2.14-0.6,3.14 c-1.06,2.64-3.47,4.71-7.2,4.71H89.09c-0.15,1.4-1.35,2.49-2.78,2.49H72.63H38.8c-1.44,0-2.63-1.1-2.78-2.49H7.8 c-3.73,0-6.14-2.07-7.2-4.71c-0.4-1-0.6-2.08-0.6-3.14c0-1.06,0.2-2.14,0.6-3.14c1.06-2.64,3.47-4.71,7.2-4.71h28.32 c0.36-1.14,1.42-1.97,2.67-1.97h8.74v-5.06c0-1.54,1.26-2.8,2.8-2.8h1.69v-5.86H24.38c-1.71,0-3.19-0.69-4.3-1.79 c-1.11-1.11-1.83-2.66-2.01-4.33l-5.33-50.74c-0.11-1.08,0.67-2.04,1.75-2.15C14.56,16.45,14.63,16.45,14.69,16.44L14.69,16.44 L14.69,16.44L14.69,16.44z M31.38,34.43h16.29v4.58H38v3.72h8.26v4.3H38v8.71h-6.61V34.43L31.38,34.43z M50.08,34.43H70.1v5.27 h-6.72v16.05h-6.59V39.7h-6.71V34.43L50.08,34.43z M73.41,34.43h10.95c2.38,0,4.17,0.57,5.35,1.7c1.19,1.14,1.78,2.75,1.78,4.84 c0,2.15-0.65,3.84-1.94,5.05c-1.3,1.21-3.27,1.82-5.93,1.82h-3.6v7.91h-6.61V34.43L73.41,34.43z M80.03,43.52h1.61 c1.27,0,2.16-0.22,2.68-0.66c0.51-0.44,0.77-1,0.77-1.69c0-0.67-0.22-1.24-0.67-1.7c-0.44-0.47-1.28-0.7-2.51-0.7h-1.88V43.52 L80.03,43.52z M56.38,81.34h10.21v-5.79H56.38V81.34L56.38,81.34z M89.11,94.88v8.27h25.97c1.97,0,3.22-1.04,3.76-2.37 c0.22-0.54,0.33-1.15,0.33-1.76s-0.11-1.22-0.33-1.76c-0.54-1.33-1.79-2.37-3.76-2.37H89.11L89.11,94.88z M36,103.15v-8.27H7.8 c-1.97,0-3.22,1.04-3.76,2.37c-0.22,0.54-0.33,1.15-0.33,1.76s0.11,1.22,0.33,1.76c0.54,1.33,1.79,2.37,3.76,2.37H36L36,103.15z M20.23,20.38h-3.35l5.1,48.57c0.08,0.78,0.39,1.47,0.87,1.95c0.4,0.4,0.92,0.64,1.53,0.64h72.54c0.59,0,1.1-0.24,1.48-0.62 c0.49-0.49,0.82-1.22,0.93-2.03l6.6-48.51L20.23,20.38L20.23,20.38L20.23,20.38z M22.19,10.6v5.83l78.46-0.83v-5H57.61 c-1.09,0-1.97-0.88-1.97-1.97V3.94H27.66v4.69c0,1.09-0.88,1.97-1.97,1.97L22.19,10.6L22.19,10.6L22.19,10.6z"/></g></svg>
|
After Width: | Height: | Size: 2.3 KiB |
91
packages/backend/src/apps/ftp/auth/index.js
Normal file
91
packages/backend/src/apps/ftp/auth/index.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import verifyCredentials from './verify-credentials.js';
|
||||
import isStillVerified from './is-still-verified.js';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'screenName',
|
||||
label: 'Screen Name',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description:
|
||||
'Screen name of your connection to be used on Automatisch UI.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'host',
|
||||
label: 'Host',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'The host information Automatisch will connect to.',
|
||||
docUrl: 'https://automatisch.io/docs/ftp#host',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Email/Username',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Your FTP login credentials.',
|
||||
docUrl: 'https://automatisch.io/docs/ftp#username',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
docUrl: 'https://automatisch.io/docs/ftp#password',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'secure',
|
||||
label: 'Secure',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
readOnly: false,
|
||||
value: false,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
docUrl: 'https://automatisch.io/docs/ftp#secure',
|
||||
clickToCopy: false,
|
||||
options: [
|
||||
{
|
||||
label: 'Yes',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: 'No',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'port',
|
||||
label: 'Port',
|
||||
type: 'string',
|
||||
required: false,
|
||||
readOnly: false,
|
||||
value: '21',
|
||||
placeholder: null,
|
||||
description: null,
|
||||
docUrl: 'https://automatisch.io/docs/ftp#port',
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
};
|
8
packages/backend/src/apps/ftp/auth/is-still-verified.js
Normal file
8
packages/backend/src/apps/ftp/auth/is-still-verified.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import verifyCredentials from './verify-credentials.js';
|
||||
|
||||
const isStillVerified = async ($) => {
|
||||
await verifyCredentials($);
|
||||
return true;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
19
packages/backend/src/apps/ftp/auth/verify-credentials.js
Normal file
19
packages/backend/src/apps/ftp/auth/verify-credentials.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Client } from 'basic-ftp';
|
||||
|
||||
const verifyCredentials = async ($) => {
|
||||
const client = new Client();
|
||||
client.ftp.verbose = true;
|
||||
|
||||
await client.access({
|
||||
host: $.auth.data.host,
|
||||
user: $.auth.data.username,
|
||||
password: $.auth.data.password,
|
||||
secure: $.auth.data.secure,
|
||||
});
|
||||
|
||||
await $.auth.set({
|
||||
screenName: $.auth.data.screenName,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
14
packages/backend/src/apps/ftp/index.js
Normal file
14
packages/backend/src/apps/ftp/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import defineApp from '../../helpers/define-app.js';
|
||||
import auth from './auth/index.js';
|
||||
|
||||
export default defineApp({
|
||||
name: 'FTP',
|
||||
key: 'ftp',
|
||||
iconUrl: '{BASE_URL}/apps/ftp/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/ftp/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: '',
|
||||
apiBaseUrl: '',
|
||||
primaryColor: '000000',
|
||||
auth,
|
||||
});
|
@@ -14,24 +14,55 @@ export default defineAction({
|
||||
value: '200',
|
||||
},
|
||||
{
|
||||
label: 'JSON body',
|
||||
key: 'stringifiedJsonBody',
|
||||
label: 'Headers',
|
||||
key: 'headers',
|
||||
type: 'dynamic',
|
||||
required: false,
|
||||
description: 'Add or remove headers as needed',
|
||||
fields: [
|
||||
{
|
||||
label: 'Key',
|
||||
key: 'key',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Header key',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Value',
|
||||
key: 'value',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Header value',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Body',
|
||||
key: 'body',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The content of the JSON body. It must be a valid JSON.',
|
||||
description: 'The content of the response body.',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const parsedStatusCode = parseInt($.step.parameters.statusCode, 10);
|
||||
const stringifiedJsonBody = $.step.parameters.stringifiedJsonBody;
|
||||
const parsedJsonBody = JSON.parse(stringifiedJsonBody);
|
||||
const statusCode = parseInt($.step.parameters.statusCode, 10);
|
||||
const body = $.step.parameters.body;
|
||||
const headers = $.step.parameters.headers.reduce((result, entry) => {
|
||||
return {
|
||||
...result,
|
||||
[entry.key]: entry.value,
|
||||
};
|
||||
}, {});
|
||||
|
||||
$.setActionItem({
|
||||
raw: {
|
||||
body: parsedJsonBody,
|
||||
statusCode: parsedStatusCode,
|
||||
headers,
|
||||
body,
|
||||
statusCode,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@@ -4,7 +4,7 @@ import Crypto from 'crypto';
|
||||
import app from '../../../../../app.js';
|
||||
import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js';
|
||||
import { createUser } from '../../../../../../test/factories/user.js';
|
||||
import getAdminAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/get-app-auth-client.js';
|
||||
import getAdminAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/app-auth-clients/get-app-auth-client.js';
|
||||
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
|
||||
import { createRole } from '../../../../../../test/factories/role.js';
|
||||
import * as license from '../../../../../helpers/license.ee.js';
|
@@ -0,0 +1,11 @@
|
||||
import { renderObject } from '../../../../../helpers/renderer.js';
|
||||
import AppAuthClient from '../../../../../models/app-auth-client.js';
|
||||
|
||||
export default async (request, response) => {
|
||||
const appAuthClients = await AppAuthClient.query().orderBy(
|
||||
'created_at',
|
||||
'desc'
|
||||
);
|
||||
|
||||
renderObject(response, appAuthClients);
|
||||
};
|
@@ -0,0 +1,41 @@
|
||||
import { vi, 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.js';
|
||||
import { createUser } from '../../../../../../test/factories/user.js';
|
||||
import getAdminAppAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/app-auth-clients/get-app-auth-clients.js';
|
||||
import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js';
|
||||
import { createRole } from '../../../../../../test/factories/role.js';
|
||||
import * as license from '../../../../../helpers/license.ee.js';
|
||||
|
||||
describe('GET /api/v1/admin/app-auth-clients', () => {
|
||||
let currentUser, currentUserRole, token;
|
||||
|
||||
describe('with valid license key', () => {
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
||||
|
||||
currentUserRole = await createRole({ key: 'admin' });
|
||||
currentUser = await createUser({ roleId: currentUserRole.id });
|
||||
|
||||
token = createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
it('should return app auth clients', async () => {
|
||||
const appAuthClientOne = await createAppAuthClient();
|
||||
const appAuthClientTwo = await createAppAuthClient();
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/v1/admin/app-auth-clients')
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = getAdminAppAuthClientsMock([
|
||||
appAuthClientTwo,
|
||||
appAuthClientOne,
|
||||
]);
|
||||
|
||||
expect(response.body).toEqual(expectedPayload);
|
||||
});
|
||||
});
|
||||
});
|
@@ -4,7 +4,7 @@ import Crypto from 'crypto';
|
||||
import app from '../../../../app.js';
|
||||
import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js';
|
||||
import { createUser } from '../../../../../test/factories/user.js';
|
||||
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/admin/get-app-auth-client.js';
|
||||
import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/app-auth-clients/get-app-auth-client.js';
|
||||
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
|
||||
import * as license from '../../../../helpers/license.ee.js';
|
||||
|
||||
|
@@ -0,0 +1,10 @@
|
||||
import { renderObject } from '../../../../helpers/renderer.js';
|
||||
import AppAuthClient from '../../../../models/app-auth-client.js';
|
||||
|
||||
export default async (request, response) => {
|
||||
const appAuthClients = await AppAuthClient.query()
|
||||
.where({ active: true })
|
||||
.orderBy('created_at', 'desc');
|
||||
|
||||
renderObject(response, appAuthClients);
|
||||
};
|
@@ -0,0 +1,37 @@
|
||||
import { vi, 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.js';
|
||||
import { createUser } from '../../../../../test/factories/user.js';
|
||||
import getAppAuthClientsMock from '../../../../../test/mocks/rest/api/v1/app-auth-clients/get-app-auth-clients.js';
|
||||
import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js';
|
||||
import * as license from '../../../../helpers/license.ee.js';
|
||||
|
||||
describe('GET /api/v1/app-auth-clients', () => {
|
||||
let currentUser, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
||||
|
||||
currentUser = await createUser();
|
||||
|
||||
token = createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
it('should return specified app auth client info', async () => {
|
||||
const appAuthClientOne = await createAppAuthClient();
|
||||
const appAuthClientTwo = await createAppAuthClient();
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/v1/app-auth-clients')
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = getAppAuthClientsMock([
|
||||
appAuthClientTwo,
|
||||
appAuthClientOne,
|
||||
]);
|
||||
|
||||
expect(response.body).toEqual(expectedPayload);
|
||||
});
|
||||
});
|
@@ -42,9 +42,12 @@ describe('GET /api/v1/executions', () => {
|
||||
|
||||
const currentUserExecutionTwo = await createExecution({
|
||||
flowId: currentUserFlow.id,
|
||||
deletedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await currentUserExecutionTwo
|
||||
.$query()
|
||||
.patchAndFetch({ deletedAt: new Date().toISOString() });
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Execution',
|
||||
@@ -87,9 +90,12 @@ describe('GET /api/v1/executions', () => {
|
||||
|
||||
const anotherUserExecutionTwo = await createExecution({
|
||||
flowId: anotherUserFlow.id,
|
||||
deletedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await anotherUserExecutionTwo
|
||||
.$query()
|
||||
.patchAndFetch({ deletedAt: new Date().toISOString() });
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Execution',
|
||||
|
@@ -0,0 +1,17 @@
|
||||
import { renderObject } from '../../../../helpers/renderer.js';
|
||||
|
||||
export default async (request, response) => {
|
||||
const step = await request.currentUser.authorizedSteps
|
||||
.clone()
|
||||
.where('steps.id', request.params.stepId)
|
||||
.whereNotNull('steps.app_key')
|
||||
.first()
|
||||
.throwIfNotFound();
|
||||
|
||||
const dynamicFields = await step.createDynamicFields(
|
||||
request.body.dynamicFieldsKey,
|
||||
request.body.parameters
|
||||
);
|
||||
|
||||
renderObject(response, dynamicFields);
|
||||
};
|
@@ -0,0 +1,169 @@
|
||||
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';
|
||||
import { createStep } from '../../../../../test/factories/step';
|
||||
import { createPermission } from '../../../../../test/factories/permission';
|
||||
import createDynamicFieldsMock from '../../../../../test/mocks/rest/api/v1/steps/create-dynamic-fields';
|
||||
|
||||
describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
||||
let currentUser, currentUserRole, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUser = await createUser();
|
||||
currentUserRole = await currentUser.$relatedQuery('role');
|
||||
|
||||
token = createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
it('should return dynamically created fields of the current users step', async () => {
|
||||
const currentUserflow = await createFlow({ userId: currentUser.id });
|
||||
|
||||
const actionStep = await createStep({
|
||||
flowId: currentUserflow.id,
|
||||
type: 'action',
|
||||
appKey: 'slack',
|
||||
key: 'sendMessageToChannel',
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: ['isCreator'],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: ['isCreator'],
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/api/v1/steps/${actionStep.id}/dynamic-fields`)
|
||||
.set('Authorization', token)
|
||||
.send({
|
||||
dynamicFieldsKey: 'listFieldsAfterSendAsBot',
|
||||
parameters: {
|
||||
sendAsBot: true,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = await createDynamicFieldsMock();
|
||||
|
||||
expect(response.body).toEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return dynamically created fields of the another users step', async () => {
|
||||
const anotherUser = await createUser();
|
||||
const anotherUserflow = await createFlow({ userId: anotherUser.id });
|
||||
|
||||
const actionStep = await createStep({
|
||||
flowId: anotherUserflow.id,
|
||||
type: 'action',
|
||||
appKey: 'slack',
|
||||
key: 'sendMessageToChannel',
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/api/v1/steps/${actionStep.id}/dynamic-fields`)
|
||||
.set('Authorization', token)
|
||||
.send({
|
||||
dynamicFieldsKey: 'listFieldsAfterSendAsBot',
|
||||
parameters: {
|
||||
sendAsBot: true,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = await createDynamicFieldsMock();
|
||||
|
||||
expect(response.body).toEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return not found response for not existing step UUID', async () => {
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const notExistingStepUUID = Crypto.randomUUID();
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return not found response for existing step UUID without app key', async () => {
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const step = await createStep({ appKey: null });
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return bad request response for invalid UUID', async () => {
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.post('/api/v1/steps/invalidStepUUID/dynamic-fields')
|
||||
.set('Authorization', token)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
@@ -0,0 +1,27 @@
|
||||
import { ref } from 'objection';
|
||||
import ExecutionStep from '../../../../models/execution-step.js';
|
||||
import { renderObject } from '../../../../helpers/renderer.js';
|
||||
|
||||
export default async (request, response) => {
|
||||
const step = await request.currentUser.authorizedSteps
|
||||
.clone()
|
||||
.findOne({ 'steps.id': request.params.stepId })
|
||||
.throwIfNotFound();
|
||||
|
||||
const previousSteps = await request.currentUser.authorizedSteps
|
||||
.clone()
|
||||
.withGraphJoined('executionSteps')
|
||||
.where('flow_id', '=', step.flowId)
|
||||
.andWhere('position', '<', step.position)
|
||||
.andWhere(
|
||||
'executionSteps.created_at',
|
||||
'=',
|
||||
ExecutionStep.query()
|
||||
.max('created_at')
|
||||
.where('step_id', '=', ref('steps.id'))
|
||||
.andWhere('status', 'success')
|
||||
)
|
||||
.orderBy('steps.position', 'asc');
|
||||
|
||||
renderObject(response, previousSteps);
|
||||
};
|
@@ -0,0 +1,173 @@
|
||||
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';
|
||||
import { createStep } from '../../../../../test/factories/step';
|
||||
import { createExecutionStep } from '../../../../../test/factories/execution-step.js';
|
||||
import { createPermission } from '../../../../../test/factories/permission';
|
||||
import getPreviousStepsMock from '../../../../../test/mocks/rest/api/v1/steps/get-previous-steps';
|
||||
|
||||
describe('GET /api/v1/steps/:stepId/previous-steps', () => {
|
||||
let currentUser, currentUserRole, token;
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUser = await createUser();
|
||||
currentUserRole = await currentUser.$relatedQuery('role');
|
||||
|
||||
token = createAuthTokenByUserId(currentUser.id);
|
||||
});
|
||||
|
||||
it('should return the previous steps of the specified step of the current user', async () => {
|
||||
const currentUserflow = await createFlow({ userId: currentUser.id });
|
||||
|
||||
const triggerStep = await createStep({
|
||||
flowId: currentUserflow.id,
|
||||
type: 'trigger',
|
||||
});
|
||||
|
||||
const actionStepOne = await createStep({
|
||||
flowId: currentUserflow.id,
|
||||
type: 'action',
|
||||
});
|
||||
|
||||
const actionStepTwo = await createStep({
|
||||
flowId: currentUserflow.id,
|
||||
type: 'action',
|
||||
});
|
||||
|
||||
const executionStepOne = await createExecutionStep({
|
||||
stepId: triggerStep.id,
|
||||
});
|
||||
|
||||
const executionStepTwo = await createExecutionStep({
|
||||
stepId: actionStepOne.id,
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: ['isCreator'],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: ['isCreator'],
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`)
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = await getPreviousStepsMock(
|
||||
[triggerStep, actionStepOne],
|
||||
[executionStepOne, executionStepTwo]
|
||||
);
|
||||
|
||||
expect(response.body).toEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return the previous steps of the specified step of another user', async () => {
|
||||
const anotherUser = await createUser();
|
||||
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
|
||||
|
||||
const triggerStep = await createStep({
|
||||
flowId: anotherUserFlow.id,
|
||||
type: 'trigger',
|
||||
});
|
||||
|
||||
const actionStepOne = await createStep({
|
||||
flowId: anotherUserFlow.id,
|
||||
type: 'action',
|
||||
});
|
||||
|
||||
const actionStepTwo = await createStep({
|
||||
flowId: anotherUserFlow.id,
|
||||
type: 'action',
|
||||
});
|
||||
|
||||
const executionStepOne = await createExecutionStep({
|
||||
stepId: triggerStep.id,
|
||||
});
|
||||
|
||||
const executionStepTwo = await createExecutionStep({
|
||||
stepId: actionStepOne.id,
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`)
|
||||
.set('Authorization', token)
|
||||
.expect(200);
|
||||
|
||||
const expectedPayload = await getPreviousStepsMock(
|
||||
[triggerStep, actionStepOne],
|
||||
[executionStepOne, executionStepTwo]
|
||||
);
|
||||
|
||||
expect(response.body).toEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return not found response for not existing step UUID', async () => {
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
const notExistingFlowUUID = Crypto.randomUUID();
|
||||
|
||||
await request(app)
|
||||
.get(`/api/v1/steps/${notExistingFlowUUID}/previous-steps`)
|
||||
.set('Authorization', token)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return bad request response for invalid UUID', async () => {
|
||||
await createPermission({
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
roleId: currentUserRole.id,
|
||||
conditions: [],
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.get('/api/v1/steps/invalidFlowUUID/previous-steps')
|
||||
.set('Authorization', token)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
@@ -26,6 +26,4 @@ export default async (request, response) => {
|
||||
}
|
||||
|
||||
await handlerSync(flowId, request, response);
|
||||
|
||||
response.sendStatus(204);
|
||||
};
|
||||
|
@@ -1,32 +0,0 @@
|
||||
import appConfig from '../../config/app.js';
|
||||
import { hasValidLicense } from '../../helpers/license.ee.js';
|
||||
import Config from '../../models/config.js';
|
||||
|
||||
const getConfig = async (_parent, params) => {
|
||||
if (!(await hasValidLicense())) return {};
|
||||
|
||||
const defaultConfig = {
|
||||
disableNotificationsPage: appConfig.disableNotificationsPage,
|
||||
disableFavicon: appConfig.disableFavicon,
|
||||
additionalDrawerLink: appConfig.additionalDrawerLink,
|
||||
additionalDrawerLinkText: appConfig.additionalDrawerLinkText,
|
||||
};
|
||||
|
||||
const configQuery = Config.query();
|
||||
|
||||
if (Array.isArray(params.keys)) {
|
||||
configQuery.whereIn('key', params.keys);
|
||||
}
|
||||
|
||||
const config = await configQuery.orderBy('key', 'asc');
|
||||
|
||||
return config.reduce((computedConfig, configEntry) => {
|
||||
const { key, value } = configEntry;
|
||||
|
||||
computedConfig[key] = value?.data;
|
||||
|
||||
return computedConfig;
|
||||
}, defaultConfig);
|
||||
};
|
||||
|
||||
export default getConfig;
|
@@ -1,140 +0,0 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import app from '../../app';
|
||||
import { createConfig } from '../../../test/factories/config';
|
||||
import appConfig from '../../config/app';
|
||||
import * as license from '../../helpers/license.ee';
|
||||
|
||||
describe('graphQL getConfig query', () => {
|
||||
let configOne, configTwo, configThree, query;
|
||||
|
||||
beforeEach(async () => {
|
||||
configOne = await createConfig({ key: 'configOne' });
|
||||
configTwo = await createConfig({ key: 'configTwo' });
|
||||
configThree = await createConfig({ key: 'configThree' });
|
||||
|
||||
query = `
|
||||
query {
|
||||
getConfig
|
||||
}
|
||||
`;
|
||||
});
|
||||
|
||||
describe('and without valid license', () => {
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(false);
|
||||
});
|
||||
|
||||
describe('and correct permissions', () => {
|
||||
it('should return empty config data', async () => {
|
||||
const response = await request(app)
|
||||
.post('/graphql')
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
|
||||
const expectedResponsePayload = { data: { getConfig: {} } };
|
||||
|
||||
expect(response.body).toEqual(expectedResponsePayload);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with valid license', () => {
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe('and without providing specific keys', () => {
|
||||
it('should return all config data', async () => {
|
||||
const response = await request(app)
|
||||
.post('/graphql')
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
|
||||
const expectedResponsePayload = {
|
||||
data: {
|
||||
getConfig: {
|
||||
[configOne.key]: configOne.value.data,
|
||||
[configTwo.key]: configTwo.value.data,
|
||||
[configThree.key]: configThree.value.data,
|
||||
disableNotificationsPage: false,
|
||||
disableFavicon: false,
|
||||
additionalDrawerLink: undefined,
|
||||
additionalDrawerLinkText: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(response.body).toEqual(expectedResponsePayload);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with providing specific keys', () => {
|
||||
it('should return all config data', async () => {
|
||||
query = `
|
||||
query {
|
||||
getConfig(keys: ["configOne", "configTwo"])
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await request(app)
|
||||
.post('/graphql')
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
|
||||
const expectedResponsePayload = {
|
||||
data: {
|
||||
getConfig: {
|
||||
[configOne.key]: configOne.value.data,
|
||||
[configTwo.key]: configTwo.value.data,
|
||||
disableNotificationsPage: false,
|
||||
disableFavicon: false,
|
||||
additionalDrawerLink: undefined,
|
||||
additionalDrawerLinkText: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(response.body).toEqual(expectedResponsePayload);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with different defaults', () => {
|
||||
beforeEach(async () => {
|
||||
vi.spyOn(appConfig, 'disableNotificationsPage', 'get').mockReturnValue(
|
||||
true
|
||||
);
|
||||
vi.spyOn(appConfig, 'disableFavicon', 'get').mockReturnValue(true);
|
||||
vi.spyOn(appConfig, 'additionalDrawerLink', 'get').mockReturnValue(
|
||||
'https://automatisch.io'
|
||||
);
|
||||
vi.spyOn(appConfig, 'additionalDrawerLinkText', 'get').mockReturnValue(
|
||||
'Automatisch'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return custom config', async () => {
|
||||
const response = await request(app)
|
||||
.post('/graphql')
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
|
||||
const expectedResponsePayload = {
|
||||
data: {
|
||||
getConfig: {
|
||||
[configOne.key]: configOne.value.data,
|
||||
[configTwo.key]: configTwo.value.data,
|
||||
[configThree.key]: configThree.value.data,
|
||||
disableNotificationsPage: true,
|
||||
disableFavicon: true,
|
||||
additionalDrawerLink: 'https://automatisch.io',
|
||||
additionalDrawerLinkText: 'Automatisch',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(response.body).toEqual(expectedResponsePayload);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -136,7 +136,7 @@ describe('graphQL getFlow query', () => {
|
||||
id: actionStep.id,
|
||||
key: 'translateText',
|
||||
parameters: {},
|
||||
position: 1,
|
||||
position: 2,
|
||||
status: actionStep.status,
|
||||
type: 'action',
|
||||
webhookUrl: 'http://localhost:3000/null',
|
||||
@@ -223,7 +223,7 @@ describe('graphQL getFlow query', () => {
|
||||
id: actionStep.id,
|
||||
key: 'translateText',
|
||||
parameters: {},
|
||||
position: 1,
|
||||
position: 2,
|
||||
status: actionStep.status,
|
||||
type: 'action',
|
||||
webhookUrl: 'http://localhost:3000/null',
|
||||
|
@@ -1,40 +0,0 @@
|
||||
import Flow from '../../models/flow.js';
|
||||
import paginate from '../../helpers/pagination.js';
|
||||
|
||||
const getFlows = async (_parent, params, context) => {
|
||||
const conditions = context.currentUser.can('read', 'Flow');
|
||||
const userFlows = context.currentUser.$relatedQuery('flows');
|
||||
const allFlows = Flow.query();
|
||||
const baseQuery = conditions.isCreator ? userFlows : allFlows;
|
||||
|
||||
const flowsQuery = baseQuery
|
||||
.clone()
|
||||
.joinRelated({
|
||||
steps: true,
|
||||
})
|
||||
.withGraphFetched({
|
||||
steps: {
|
||||
connection: true,
|
||||
},
|
||||
})
|
||||
.where((builder) => {
|
||||
if (params.connectionId) {
|
||||
builder.where('steps.connection_id', params.connectionId);
|
||||
}
|
||||
|
||||
if (params.name) {
|
||||
builder.where('flows.name', 'ilike', `%${params.name}%`);
|
||||
}
|
||||
|
||||
if (params.appKey) {
|
||||
builder.where('steps.app_key', params.appKey);
|
||||
}
|
||||
})
|
||||
.groupBy('flows.id')
|
||||
.orderBy('active', 'desc')
|
||||
.orderBy('updated_at', 'desc');
|
||||
|
||||
return paginate(flowsQuery, params.limit, params.offset);
|
||||
};
|
||||
|
||||
export default getFlows;
|
@@ -1,16 +0,0 @@
|
||||
import axios from '../../helpers/axios-with-proxy.js';
|
||||
|
||||
const NOTIFICATIONS_URL =
|
||||
'https://notifications.automatisch.io/notifications.json';
|
||||
|
||||
const getNotifications = async () => {
|
||||
try {
|
||||
const { data: notifications = [] } = await axios.get(NOTIFICATIONS_URL);
|
||||
|
||||
return notifications;
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default getNotifications;
|
@@ -1,19 +0,0 @@
|
||||
import paginate from '../../helpers/pagination.js';
|
||||
import User from '../../models/user.js';
|
||||
|
||||
const getUsers = async (_parent, params, context) => {
|
||||
context.currentUser.can('read', 'User');
|
||||
|
||||
const usersQuery = User.query()
|
||||
.leftJoinRelated({
|
||||
role: true,
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true,
|
||||
})
|
||||
.orderBy('full_name', 'asc');
|
||||
|
||||
return paginate(usersQuery, params.limit, params.offset);
|
||||
};
|
||||
|
||||
export default getUsers;
|
@@ -1,148 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import app from '../../app';
|
||||
import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id';
|
||||
import { createRole } from '../../../test/factories/role';
|
||||
import { createPermission } from '../../../test/factories/permission';
|
||||
import { createUser } from '../../../test/factories/user';
|
||||
|
||||
describe('graphQL getUsers query', () => {
|
||||
const query = `
|
||||
query {
|
||||
getUsers(limit: 10, offset: 0) {
|
||||
pageInfo {
|
||||
currentPage
|
||||
totalPages
|
||||
}
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fullName
|
||||
email
|
||||
role {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('and without permissions', () => {
|
||||
it('should throw not authorized error', async () => {
|
||||
const userWithoutPermissions = await createUser();
|
||||
const token = createAuthTokenByUserId(userWithoutPermissions.id);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/graphql')
|
||||
.set('Authorization', token)
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toEqual('Not authorized!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with correct permissions', () => {
|
||||
let role, currentUser, anotherUser, token, requestObject;
|
||||
|
||||
beforeEach(async () => {
|
||||
role = await createRole({
|
||||
key: 'sample',
|
||||
name: 'sample',
|
||||
});
|
||||
|
||||
await createPermission({
|
||||
action: 'read',
|
||||
subject: 'User',
|
||||
roleId: role.id,
|
||||
});
|
||||
|
||||
currentUser = await createUser({
|
||||
roleId: role.id,
|
||||
fullName: 'Current User',
|
||||
});
|
||||
|
||||
anotherUser = await createUser({
|
||||
roleId: role.id,
|
||||
fullName: 'Another User',
|
||||
});
|
||||
|
||||
token = createAuthTokenByUserId(currentUser.id);
|
||||
requestObject = request(app).post('/graphql').set('Authorization', token);
|
||||
});
|
||||
|
||||
it('should return users data', async () => {
|
||||
const response = await requestObject.send({ query }).expect(200);
|
||||
|
||||
const expectedResponsePayload = {
|
||||
data: {
|
||||
getUsers: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
email: anotherUser.email,
|
||||
fullName: anotherUser.fullName,
|
||||
id: anotherUser.id,
|
||||
role: {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
email: currentUser.email,
|
||||
fullName: currentUser.fullName,
|
||||
id: currentUser.id,
|
||||
role: {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
},
|
||||
totalCount: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(response.body).toEqual(expectedResponsePayload);
|
||||
});
|
||||
|
||||
it('should not return users data with password', async () => {
|
||||
const query = `
|
||||
query {
|
||||
getUsers(limit: 10, offset: 0) {
|
||||
pageInfo {
|
||||
currentPage
|
||||
totalPages
|
||||
}
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fullName
|
||||
password
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await requestObject.send({ query }).expect(400);
|
||||
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toEqual(
|
||||
'Cannot query field "password" on type "User".'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -2,15 +2,11 @@ import getApp from './queries/get-app.js';
|
||||
import getAppAuthClient from './queries/get-app-auth-client.ee.js';
|
||||
import getAppAuthClients from './queries/get-app-auth-clients.ee.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';
|
||||
import getDynamicData from './queries/get-dynamic-data.js';
|
||||
import getDynamicFields from './queries/get-dynamic-fields.js';
|
||||
import getFlow from './queries/get-flow.js';
|
||||
import getFlows from './queries/get-flows.js';
|
||||
import getNotifications from './queries/get-notifications.js';
|
||||
import getStepWithTestExecutions from './queries/get-step-with-test-executions.js';
|
||||
import getUsers from './queries/get-users.js';
|
||||
import testConnection from './queries/test-connection.js';
|
||||
|
||||
const queryResolvers = {
|
||||
@@ -18,15 +14,11 @@ const queryResolvers = {
|
||||
getAppAuthClient,
|
||||
getAppAuthClients,
|
||||
getBillingAndUsage,
|
||||
getConfig,
|
||||
getConnectedApps,
|
||||
getDynamicData,
|
||||
getDynamicFields,
|
||||
getFlow,
|
||||
getFlows,
|
||||
getNotifications,
|
||||
getStepWithTestExecutions,
|
||||
getUsers,
|
||||
testConnection,
|
||||
};
|
||||
|
||||
|
@@ -5,13 +5,6 @@ type Query {
|
||||
getConnectedApps(name: String): [App]
|
||||
testConnection(id: String!): Connection
|
||||
getFlow(id: String!): Flow
|
||||
getFlows(
|
||||
limit: Int!
|
||||
offset: Int!
|
||||
appKey: String
|
||||
connectionId: String
|
||||
name: String
|
||||
): FlowConnection
|
||||
getStepWithTestExecutions(stepId: String!): [Step]
|
||||
getDynamicData(
|
||||
stepId: String!
|
||||
@@ -24,9 +17,6 @@ type Query {
|
||||
parameters: JSONObject
|
||||
): [SubstepArgument]
|
||||
getBillingAndUsage: GetBillingAndUsage
|
||||
getConfig(keys: [String]): JSONObject
|
||||
getNotifications: [Notification]
|
||||
getUsers(limit: Int!, offset: Int!): UserConnection
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@@ -257,15 +247,6 @@ type Field {
|
||||
options: [SubstepArgumentOption]
|
||||
}
|
||||
|
||||
type FlowConnection {
|
||||
edges: [FlowEdge]
|
||||
pageInfo: PageInfo
|
||||
}
|
||||
|
||||
type FlowEdge {
|
||||
node: Flow
|
||||
}
|
||||
|
||||
enum FlowStatus {
|
||||
paused
|
||||
published
|
||||
@@ -304,16 +285,6 @@ type SamlAuthProvidersRoleMapping {
|
||||
remoteRoleName: String
|
||||
}
|
||||
|
||||
type UserConnection {
|
||||
edges: [UserEdge]
|
||||
pageInfo: PageInfo
|
||||
totalCount: Int
|
||||
}
|
||||
|
||||
type UserEdge {
|
||||
node: User
|
||||
}
|
||||
|
||||
input CreateConnectionInput {
|
||||
key: String!
|
||||
appAuthClientId: String
|
||||
@@ -692,13 +663,6 @@ input UpdateAppAuthClientInput {
|
||||
active: Boolean
|
||||
}
|
||||
|
||||
type Notification {
|
||||
name: String
|
||||
createdAt: String
|
||||
documentationUrl: String
|
||||
description: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
|
@@ -42,8 +42,6 @@ const isAuthenticatedRule = rule()(isAuthenticated);
|
||||
export const authenticationRules = {
|
||||
Query: {
|
||||
'*': isAuthenticatedRule,
|
||||
getConfig: allow,
|
||||
getNotifications: allow,
|
||||
},
|
||||
Mutation: {
|
||||
'*': isAuthenticatedRule,
|
||||
|
@@ -19,6 +19,14 @@ const authorizationList = {
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
},
|
||||
'GET /api/v1/steps/:stepId/previous-steps': {
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
},
|
||||
'POST /api/v1/steps/:stepId/dynamic-fields': {
|
||||
action: 'update',
|
||||
subject: 'Flow',
|
||||
},
|
||||
'GET /api/v1/connections/:connectionId/flows': {
|
||||
action: 'read',
|
||||
subject: 'Flow',
|
||||
|
@@ -75,9 +75,20 @@ export default async (flowId, request, response) => {
|
||||
});
|
||||
|
||||
if (actionStep.key === 'respondWith' && !response.headersSent) {
|
||||
const { headers, statusCode, body } = actionExecutionStep.dataOut;
|
||||
|
||||
// we set the custom response headers
|
||||
if (headers) {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (key) {
|
||||
response.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we send the response only if it's not sent yet. This allows us to early respond from the flow.
|
||||
response.status(actionExecutionStep.dataOut.statusCode);
|
||||
response.send(actionExecutionStep.dataOut.body);
|
||||
response.status(statusCode);
|
||||
response.send(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import Connection from './connection.js';
|
||||
import ExecutionStep from './execution-step.js';
|
||||
import Telemetry from '../helpers/telemetry/index.js';
|
||||
import appConfig from '../config/app.js';
|
||||
import globalVariable from '../helpers/global-variable.js';
|
||||
|
||||
class Step extends Base {
|
||||
static tableName = 'steps';
|
||||
@@ -196,6 +197,26 @@ class Step extends Base {
|
||||
return existingArguments;
|
||||
}
|
||||
|
||||
async createDynamicFields(dynamicFieldsKey, parameters) {
|
||||
const connection = await this.$relatedQuery('connection');
|
||||
const flow = await this.$relatedQuery('flow');
|
||||
const app = await this.getApp();
|
||||
const $ = await globalVariable({ connection, app, flow, step: this });
|
||||
|
||||
const command = app.dynamicFields.find(
|
||||
(data) => data.key === dynamicFieldsKey
|
||||
);
|
||||
|
||||
for (const parameterKey in parameters) {
|
||||
const parameterValue = parameters[parameterKey];
|
||||
$.step.parameters[parameterKey] = parameterValue;
|
||||
}
|
||||
|
||||
const dynamicFields = (await command.run($)) || [];
|
||||
|
||||
return dynamicFields;
|
||||
}
|
||||
|
||||
async updateWebhookUrl() {
|
||||
if (this.isAction) return this;
|
||||
|
||||
|
@@ -3,16 +3,25 @@ import asyncHandler from 'express-async-handler';
|
||||
import { authenticateUser } from '../../../../helpers/authentication.js';
|
||||
import { authorizeAdmin } from '../../../../helpers/authorization.js';
|
||||
import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js';
|
||||
import getAdminAppAuthClientsAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-client.js';
|
||||
import getAdminAppAuthClientsAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-clients.ee.js';
|
||||
import getAdminAppAuthClientAction from '../../../../controllers/api/v1/admin/app-auth-clients/get-app-auth-client.ee.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/:appAuthClientId',
|
||||
'/',
|
||||
authenticateUser,
|
||||
authorizeAdmin,
|
||||
checkIsEnterprise,
|
||||
asyncHandler(getAdminAppAuthClientsAction)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:appAuthClientId',
|
||||
authenticateUser,
|
||||
authorizeAdmin,
|
||||
checkIsEnterprise,
|
||||
asyncHandler(getAdminAppAuthClientAction)
|
||||
);
|
||||
|
||||
export default router;
|
@@ -3,9 +3,17 @@ import asyncHandler from 'express-async-handler';
|
||||
import { authenticateUser } from '../../../helpers/authentication.js';
|
||||
import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js';
|
||||
import getAppAuthClientAction from '../../../controllers/api/v1/app-auth-clients/get-app-auth-client.js';
|
||||
import getAppAuthClientsAction from '../../../controllers/api/v1/app-auth-clients/get-app-auth-clients.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authenticateUser,
|
||||
checkIsEnterprise,
|
||||
asyncHandler(getAppAuthClientsAction)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:appAuthClientId',
|
||||
authenticateUser,
|
||||
|
@@ -3,6 +3,8 @@ import asyncHandler from 'express-async-handler';
|
||||
import { authenticateUser } from '../../../helpers/authentication.js';
|
||||
import { authorizeUser } from '../../../helpers/authorization.js';
|
||||
import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js';
|
||||
import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js';
|
||||
import createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -13,4 +15,18 @@ router.get(
|
||||
asyncHandler(getConnectionAction)
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:stepId/previous-steps',
|
||||
authenticateUser,
|
||||
authorizeUser,
|
||||
asyncHandler(getPreviousStepsAction)
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/:stepId/dynamic-fields',
|
||||
authenticateUser,
|
||||
authorizeUser,
|
||||
asyncHandler(createDynamicFieldsAction)
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
@@ -18,7 +18,7 @@ import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.
|
||||
import rolesRouter from './api/v1/admin/roles.ee.js';
|
||||
import permissionsRouter from './api/v1/admin/permissions.ee.js';
|
||||
import adminUsersRouter from './api/v1/admin/users.ee.js';
|
||||
import adminAppAuthClientsRouter from './api/v1/admin/app-auth-clients.js';
|
||||
import adminAppAuthClientsRouter from './api/v1/admin/app-auth-clients.ee.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import executionStepSerializer from './execution-step.js';
|
||||
|
||||
const stepSerializer = (step) => {
|
||||
return {
|
||||
let stepData = {
|
||||
id: step.id,
|
||||
type: step.type,
|
||||
key: step.key,
|
||||
@@ -10,6 +12,14 @@ const stepSerializer = (step) => {
|
||||
position: step.position,
|
||||
parameters: step.parameters,
|
||||
};
|
||||
|
||||
if (step.executionSteps?.length > 0) {
|
||||
stepData.executionSteps = step.executionSteps.map((executionStep) =>
|
||||
executionStepSerializer(executionStep)
|
||||
);
|
||||
}
|
||||
|
||||
return stepData;
|
||||
};
|
||||
|
||||
export default stepSerializer;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createStep } from '../../test/factories/step';
|
||||
import { createExecutionStep } from '../../test/factories/execution-step';
|
||||
import stepSerializer from './step';
|
||||
import executionStepSerializer from './execution-step';
|
||||
|
||||
describe('stepSerializer', () => {
|
||||
let step;
|
||||
@@ -24,4 +26,20 @@ describe('stepSerializer', () => {
|
||||
|
||||
expect(stepSerializer(step)).toEqual(expectedPayload);
|
||||
});
|
||||
|
||||
it('should return step data with the execution steps', async () => {
|
||||
const executionStepOne = await createExecutionStep({ stepId: step.id });
|
||||
const executionStepTwo = await createExecutionStep({ stepId: step.id });
|
||||
|
||||
step.executionSteps = [executionStepOne, executionStepTwo];
|
||||
|
||||
const expectedPayload = {
|
||||
executionSteps: [
|
||||
executionStepSerializer(executionStepOne),
|
||||
executionStepSerializer(executionStepTwo),
|
||||
],
|
||||
};
|
||||
|
||||
expect(stepSerializer(step)).toMatchObject(expectedPayload);
|
||||
});
|
||||
});
|
||||
|
@@ -17,9 +17,7 @@ export const createAppAuthClient = async (params = {}) => {
|
||||
params.formattedAuthDefaults =
|
||||
params?.formattedAuthDefaults || formattedAuthDefaults;
|
||||
|
||||
const appAuthClient = await AppAuthClient.query()
|
||||
.insert(params)
|
||||
.returning('*');
|
||||
const appAuthClient = await AppAuthClient.query().insertAndFetch(params);
|
||||
|
||||
return appAuthClient;
|
||||
};
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import AppConfig from '../../src/models/app-config.js';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
export const createAppConfig = async (params = {}) => {
|
||||
params.key = params?.key || 'gitlab';
|
||||
params.key = params?.key || faker.lorem.word();
|
||||
|
||||
const appConfig = await AppConfig.query().insert(params).returning('*');
|
||||
const appConfig = await AppConfig.query().insertAndFetch(params);
|
||||
|
||||
return appConfig;
|
||||
};
|
||||
|
@@ -7,7 +7,7 @@ export const createConfig = async (params = {}) => {
|
||||
value: params?.value || { data: 'sampleConfig' },
|
||||
};
|
||||
|
||||
const config = await Config.query().insert(configData).returning('*');
|
||||
const config = await Config.query().insertAndFetch(configData);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
@@ -9,9 +9,7 @@ export const createExecutionStep = async (params = {}) => {
|
||||
params.dataIn = params?.dataIn || { dataIn: 'dataIn' };
|
||||
params.dataOut = params?.dataOut || { dataOut: 'dataOut' };
|
||||
|
||||
const executionStep = await ExecutionStep.query()
|
||||
.insert(params)
|
||||
.returning('*');
|
||||
const executionStep = await ExecutionStep.query().insertAndFetch(params);
|
||||
|
||||
return executionStep;
|
||||
};
|
||||
|
@@ -4,10 +4,8 @@ import { createFlow } from './flow';
|
||||
export const createExecution = async (params = {}) => {
|
||||
params.flowId = params?.flowId || (await createFlow()).id;
|
||||
params.testRun = params?.testRun || false;
|
||||
params.createdAt = params?.createdAt || new Date().toISOString();
|
||||
params.updatedAt = params?.updatedAt || new Date().toISOString();
|
||||
|
||||
const execution = await Execution.query().insert(params).returning('*');
|
||||
const execution = await Execution.query().insertAndFetch(params);
|
||||
|
||||
return execution;
|
||||
};
|
||||
|
@@ -7,7 +7,7 @@ export const createFlow = async (params = {}) => {
|
||||
params.createdAt = params?.createdAt || new Date().toISOString();
|
||||
params.updatedAt = params?.updatedAt || new Date().toISOString();
|
||||
|
||||
const flow = await Flow.query().insert(params).returning('*');
|
||||
const flow = await Flow.query().insertAndFetch(params);
|
||||
|
||||
return flow;
|
||||
};
|
||||
|
@@ -7,7 +7,7 @@ export const createPermission = async (params = {}) => {
|
||||
params.subject = params?.subject || 'User';
|
||||
params.conditions = params?.conditions || ['isCreator'];
|
||||
|
||||
const permission = await Permission.query().insert(params).returning('*');
|
||||
const permission = await Permission.query().insertAndFetch(params);
|
||||
|
||||
return permission;
|
||||
};
|
||||
|
@@ -4,7 +4,7 @@ export const createRole = async (params = {}) => {
|
||||
params.name = params?.name || 'Viewer';
|
||||
params.key = params?.key || 'viewer';
|
||||
|
||||
const role = await Role.query().insert(params).returning('*');
|
||||
const role = await Role.query().insertAndFetch(params);
|
||||
|
||||
return role;
|
||||
};
|
||||
|
@@ -25,9 +25,9 @@ export const createSamlAuthProvider = async (params = {}) => {
|
||||
params.defaultRoleId = params?.defaultRoleId || (await createRole()).id;
|
||||
params.active = params?.active || true;
|
||||
|
||||
const samlAuthProvider = await SamlAuthProvider.query()
|
||||
.insert(params)
|
||||
.returning('*');
|
||||
const samlAuthProvider = await SamlAuthProvider.query().insertAndFetch(
|
||||
params
|
||||
);
|
||||
|
||||
return samlAuthProvider;
|
||||
};
|
||||
|
@@ -5,19 +5,21 @@ export const createStep = async (params = {}) => {
|
||||
params.flowId = params?.flowId || (await createFlow()).id;
|
||||
params.type = params?.type || 'action';
|
||||
|
||||
const lastStep = await global.knex
|
||||
.table('steps')
|
||||
.where('flowId', params.flowId)
|
||||
.andWhere('deletedAt', '!=', null)
|
||||
.orderBy('createdAt', 'desc')
|
||||
const lastStep = await Step.query()
|
||||
.where('flow_id', params.flowId)
|
||||
.andWhere('deleted_at', null)
|
||||
.orderBy('position', 'desc')
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
params.position = params?.position || (lastStep?.position || 0) + 1;
|
||||
params.position =
|
||||
params?.position || (lastStep?.position ? lastStep.position + 1 : 1);
|
||||
|
||||
params.status = params?.status || 'completed';
|
||||
params.appKey =
|
||||
params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook');
|
||||
|
||||
const step = await Step.query().insert(params).returning('*');
|
||||
const step = await Step.query().insertAndFetch(params);
|
||||
|
||||
return step;
|
||||
};
|
||||
|
@@ -15,7 +15,7 @@ export const createSubscription = async (params = {}) => {
|
||||
params.nextBillDate =
|
||||
params?.nextBillDate || DateTime.now().plus({ days: 30 }).toISODate();
|
||||
|
||||
const subscription = await Subscription.query().insert(params).returning('*');
|
||||
const subscription = await Subscription.query().insertAndFetch(params);
|
||||
|
||||
return subscription;
|
||||
};
|
||||
|
@@ -8,7 +8,7 @@ export const createUser = async (params = {}) => {
|
||||
params.email = params?.email || faker.internet.email();
|
||||
params.password = params?.password || faker.internet.password();
|
||||
|
||||
const user = await User.query().insert(params).returning('*');
|
||||
const user = await User.query().insertAndFetch(params);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
@@ -0,0 +1,19 @@
|
||||
const getAdminAppAuthClientsMock = (appAuthClients) => {
|
||||
return {
|
||||
data: appAuthClients.map((appAuthClient) => ({
|
||||
appConfigId: appAuthClient.appConfigId,
|
||||
name: appAuthClient.name,
|
||||
id: appAuthClient.id,
|
||||
active: appAuthClient.active,
|
||||
})),
|
||||
meta: {
|
||||
count: appAuthClients.length,
|
||||
currentPage: null,
|
||||
isArray: true,
|
||||
totalPages: null,
|
||||
type: 'AppAuthClient',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getAdminAppAuthClientsMock;
|
@@ -0,0 +1,19 @@
|
||||
const getAppAuthClientsMock = (appAuthClients) => {
|
||||
return {
|
||||
data: appAuthClients.map((appAuthClient) => ({
|
||||
appConfigId: appAuthClient.appConfigId,
|
||||
name: appAuthClient.name,
|
||||
id: appAuthClient.id,
|
||||
active: appAuthClient.active,
|
||||
})),
|
||||
meta: {
|
||||
count: appAuthClients.length,
|
||||
currentPage: null,
|
||||
isArray: true,
|
||||
totalPages: null,
|
||||
type: 'AppAuthClient',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getAppAuthClientsMock;
|
@@ -0,0 +1,36 @@
|
||||
const createDynamicFieldsMock = async () => {
|
||||
const data = [
|
||||
{
|
||||
label: 'Bot name',
|
||||
key: 'botName',
|
||||
type: 'string',
|
||||
required: true,
|
||||
value: 'Automatisch',
|
||||
description:
|
||||
'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Bot icon',
|
||||
key: 'botIcon',
|
||||
type: 'string',
|
||||
required: false,
|
||||
description:
|
||||
'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
data: data,
|
||||
meta: {
|
||||
count: data.length,
|
||||
currentPage: null,
|
||||
isArray: true,
|
||||
totalPages: null,
|
||||
type: 'Object',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default createDynamicFieldsMock;
|
@@ -0,0 +1,41 @@
|
||||
const getPreviousStepsMock = async (steps, executionSteps) => {
|
||||
const data = steps.map((step) => {
|
||||
const filteredExecutionSteps = executionSteps.filter(
|
||||
(executionStep) => executionStep.stepId === step.id
|
||||
);
|
||||
|
||||
return {
|
||||
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,
|
||||
executionSteps: filteredExecutionSteps.map((executionStep) => ({
|
||||
id: executionStep.id,
|
||||
dataIn: executionStep.dataIn,
|
||||
dataOut: executionStep.dataOut,
|
||||
errorDetails: executionStep.errorDetails,
|
||||
status: executionStep.status,
|
||||
createdAt: executionStep.createdAt.getTime(),
|
||||
updatedAt: executionStep.updatedAt.getTime(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: data,
|
||||
meta: {
|
||||
count: data.length,
|
||||
currentPage: null,
|
||||
isArray: true,
|
||||
totalPages: null,
|
||||
type: 'Step',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default getPreviousStepsMock;
|
@@ -113,6 +113,12 @@ export default defineConfig({
|
||||
{ text: 'Connection', link: '/apps/formatter/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'FTP',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [{ text: 'Connection', link: '/apps/ftp/connection' }],
|
||||
},
|
||||
{
|
||||
text: 'Ghost',
|
||||
collapsible: true,
|
||||
|
16
packages/docs/pages/apps/ftp/connection.md
Normal file
16
packages/docs/pages/apps/ftp/connection.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# FTP
|
||||
|
||||
:::info
|
||||
This page explains the steps you need to follow to set up the FTP connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||
:::
|
||||
|
||||
FTP, which stands for File Transfer Protocol, is a standard network protocol used for the transfer of computer files between a client and server on a computer network. t's commonly used to download files from a server or upload files to a server.. You need to provide the following information from Automatisch by using FTP connection.
|
||||
|
||||
1. Write any screen name to be displayed in Automatisch.
|
||||
1. Fill host address field with the FTP host address.
|
||||
1. Fill username field with the FTP username.
|
||||
1. Fill password field with the FTP password.
|
||||
1. Select the Secure field.
|
||||
1. Fill port field with the FTP port. Default is 21.
|
||||
1. Click **Submit** button on Automatisch.
|
||||
1. Now, you can start using the FTP connection with Automatisch.
|
1
packages/docs/pages/public/favicons/ftp.svg
Normal file
1
packages/docs/pages/public/favicons/ftp.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 109.36" style="enable-background:new 0 0 122.88 109.36" xml:space="preserve"><g><path d="M14.69,16.44h3.56V8.63c0-1.09,0.88-1.97,1.97-1.97h3.5V1.97C23.72,0.88,24.6,0,25.69,0h31.92c1.09,0,1.97,0.88,1.97,1.97 v4.69h43.05c1.09,0,1.97,0.88,1.97,1.97v7.82h3.59c1.09,0,1.97,0.88,1.97,1.97c0,0.14-0.01,0.28-0.04,0.42l-6.88,50.59 c-0.22,1.65-0.95,3.18-2.05,4.29c-1.1,1.1-2.54,1.78-4.25,1.78H70.95v5.86h2.12c1.54,0,2.8,1.26,2.8,2.8v5.06h10.44 c1.25,0,2.32,0.83,2.67,1.97h26.1c3.73,0,6.14,2.07,7.2,4.71c0.4,1,0.6,2.08,0.6,3.14c0,1.06-0.2,2.14-0.6,3.14 c-1.06,2.64-3.47,4.71-7.2,4.71H89.09c-0.15,1.4-1.35,2.49-2.78,2.49H72.63H38.8c-1.44,0-2.63-1.1-2.78-2.49H7.8 c-3.73,0-6.14-2.07-7.2-4.71c-0.4-1-0.6-2.08-0.6-3.14c0-1.06,0.2-2.14,0.6-3.14c1.06-2.64,3.47-4.71,7.2-4.71h28.32 c0.36-1.14,1.42-1.97,2.67-1.97h8.74v-5.06c0-1.54,1.26-2.8,2.8-2.8h1.69v-5.86H24.38c-1.71,0-3.19-0.69-4.3-1.79 c-1.11-1.11-1.83-2.66-2.01-4.33l-5.33-50.74c-0.11-1.08,0.67-2.04,1.75-2.15C14.56,16.45,14.63,16.45,14.69,16.44L14.69,16.44 L14.69,16.44L14.69,16.44z M31.38,34.43h16.29v4.58H38v3.72h8.26v4.3H38v8.71h-6.61V34.43L31.38,34.43z M50.08,34.43H70.1v5.27 h-6.72v16.05h-6.59V39.7h-6.71V34.43L50.08,34.43z M73.41,34.43h10.95c2.38,0,4.17,0.57,5.35,1.7c1.19,1.14,1.78,2.75,1.78,4.84 c0,2.15-0.65,3.84-1.94,5.05c-1.3,1.21-3.27,1.82-5.93,1.82h-3.6v7.91h-6.61V34.43L73.41,34.43z M80.03,43.52h1.61 c1.27,0,2.16-0.22,2.68-0.66c0.51-0.44,0.77-1,0.77-1.69c0-0.67-0.22-1.24-0.67-1.7c-0.44-0.47-1.28-0.7-2.51-0.7h-1.88V43.52 L80.03,43.52z M56.38,81.34h10.21v-5.79H56.38V81.34L56.38,81.34z M89.11,94.88v8.27h25.97c1.97,0,3.22-1.04,3.76-2.37 c0.22-0.54,0.33-1.15,0.33-1.76s-0.11-1.22-0.33-1.76c-0.54-1.33-1.79-2.37-3.76-2.37H89.11L89.11,94.88z M36,103.15v-8.27H7.8 c-1.97,0-3.22,1.04-3.76,2.37c-0.22,0.54-0.33,1.15-0.33,1.76s0.11,1.22,0.33,1.76c0.54,1.33,1.79,2.37,3.76,2.37H36L36,103.15z M20.23,20.38h-3.35l5.1,48.57c0.08,0.78,0.39,1.47,0.87,1.95c0.4,0.4,0.92,0.64,1.53,0.64h72.54c0.59,0,1.1-0.24,1.48-0.62 c0.49-0.49,0.82-1.22,0.93-2.03l6.6-48.51L20.23,20.38L20.23,20.38L20.23,20.38z M22.19,10.6v5.83l78.46-0.83v-5H57.61 c-1.09,0-1.97-0.88-1.97-1.97V3.94H27.66v4.69c0,1.09-0.88,1.97-1.97,1.97L22.19,10.6L22.19,10.6L22.19,10.6z"/></g></svg>
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1,35 +1,39 @@
|
||||
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';
|
||||
const FLOW_PER_PAGE = 10;
|
||||
const getLimitAndOffset = (page) => ({
|
||||
limit: FLOW_PER_PAGE,
|
||||
offset: (page - 1) * FLOW_PER_PAGE,
|
||||
});
|
||||
import useConnectionFlows from 'hooks/useConnectionFlows';
|
||||
import useAppFlows from 'hooks/useAppFlows';
|
||||
|
||||
function AppFlows(props) {
|
||||
const { appKey } = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const connectionId = searchParams.get('connectionId') || undefined;
|
||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||
const { data } = useQuery(GET_FLOWS, {
|
||||
variables: {
|
||||
appKey,
|
||||
connectionId,
|
||||
...getLimitAndOffset(page),
|
||||
},
|
||||
});
|
||||
const getFlows = data?.getFlows || {};
|
||||
const { pageInfo, edges } = getFlows;
|
||||
const flows = edges?.map(({ node }) => node);
|
||||
const isConnectionFlowEnabled = !!connectionId;
|
||||
const isAppFlowEnabled = !!appKey && !connectionId;
|
||||
|
||||
const connectionFlows = useConnectionFlows(
|
||||
{ connectionId, page },
|
||||
{ enabled: isConnectionFlowEnabled },
|
||||
);
|
||||
|
||||
const appFlows = useAppFlows({ appKey, page }, { enabled: isAppFlowEnabled });
|
||||
|
||||
const flows = isConnectionFlowEnabled
|
||||
? connectionFlows?.data?.data || []
|
||||
: appFlows?.data?.data || [];
|
||||
const pageInfo = isConnectionFlowEnabled
|
||||
? connectionFlows?.data?.meta || []
|
||||
: appFlows?.data?.meta || [];
|
||||
const hasFlows = flows?.length;
|
||||
|
||||
if (!hasFlows) {
|
||||
return (
|
||||
<NoResultFound
|
||||
@@ -39,6 +43,7 @@ function AppFlows(props) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{flows?.map((appFlow) => (
|
||||
|
@@ -1,10 +1,14 @@
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||
import { LogoImage } from './style.ee';
|
||||
|
||||
const CustomLogo = () => {
|
||||
const { config, loading } = useConfig(['logo.svgData']);
|
||||
if (loading || !config?.['logo.svgData']) return null;
|
||||
const { data: configData, isLoading } = useAutomatischConfig();
|
||||
const config = configData?.data;
|
||||
|
||||
if (isLoading || !config?.['logo.svgData']) return null;
|
||||
|
||||
const logoSvgData = config['logo.svgData'];
|
||||
|
||||
return (
|
||||
<LogoImage
|
||||
data-test="custom-logo"
|
||||
|
@@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import * as React from 'react';
|
||||
import ConfirmationDialog from 'components/ConfirmationDialog';
|
||||
@@ -17,9 +19,13 @@ function DeleteUserButton(props) {
|
||||
});
|
||||
const formatMessage = useFormatMessage();
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleConfirm = React.useCallback(async () => {
|
||||
try {
|
||||
await deleteUser();
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'user', userId] });
|
||||
setShowConfirmation(false);
|
||||
enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), {
|
||||
variant: 'success',
|
||||
@@ -31,6 +37,7 @@ function DeleteUserButton(props) {
|
||||
throw new Error('Failed while deleting!');
|
||||
}
|
||||
}, [deleteUser]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
|
@@ -15,7 +15,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useVersion from 'hooks/useVersion';
|
||||
import AppBar from 'components/AppBar';
|
||||
import Drawer from 'components/Drawer';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||
|
||||
const drawerLinks = [
|
||||
{
|
||||
@@ -77,11 +77,9 @@ const generateDrawerBottomLinks = async ({
|
||||
|
||||
export default function PublicLayout({ children }) {
|
||||
const version = useVersion();
|
||||
const { config, loading } = useConfig([
|
||||
'disableNotificationsPage',
|
||||
'additionalDrawerLink',
|
||||
'additionalDrawerLinkText',
|
||||
]);
|
||||
const { data: configData, isLoading } = useAutomatischConfig();
|
||||
const config = configData?.data;
|
||||
|
||||
const theme = useTheme();
|
||||
const formatMessage = useFormatMessage();
|
||||
const [bottomLinks, setBottomLinks] = React.useState([]);
|
||||
@@ -102,10 +100,10 @@ export default function PublicLayout({ children }) {
|
||||
setBottomLinks(newBottomLinks);
|
||||
}
|
||||
|
||||
if (loading) return;
|
||||
if (isLoading) return;
|
||||
|
||||
perform();
|
||||
}, [config, loading, version.newVersionCount]);
|
||||
}, [config, isLoading, version.newVersionCount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@@ -1,12 +1,19 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import CustomLogo from 'components/CustomLogo/index.ee';
|
||||
import DefaultLogo from 'components/DefaultLogo';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||
|
||||
const Logo = () => {
|
||||
const { config, loading } = useConfig(['logo.svgData']);
|
||||
const { data: configData, isLoading } = useAutomatischConfig();
|
||||
const config = configData?.data;
|
||||
const logoSvgData = config?.['logo.svgData'];
|
||||
if (loading && !logoSvgData) return <React.Fragment />;
|
||||
|
||||
if (isLoading && !logoSvgData) return <React.Fragment />;
|
||||
|
||||
if (logoSvgData) return <CustomLogo />;
|
||||
|
||||
return <DefaultLogo />;
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
|
@@ -1,15 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||
|
||||
const MetadataProvider = ({ children }) => {
|
||||
const { config } = useConfig();
|
||||
const { data: configData } = useAutomatischConfig();
|
||||
const config = configData?.data;
|
||||
|
||||
React.useEffect(() => {
|
||||
document.title = config?.title || 'Automatisch';
|
||||
}, [config?.title]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const existingFaviconElement = document.querySelector("link[rel~='icon']");
|
||||
|
||||
if (config?.disableFavicon === true) {
|
||||
existingFaviconElement?.remove();
|
||||
}
|
||||
|
||||
if (config?.disableFavicon === false) {
|
||||
if (existingFaviconElement) {
|
||||
existingFaviconElement.href = '/browser-tab.ico';
|
||||
@@ -20,7 +27,10 @@ const MetadataProvider = ({ children }) => {
|
||||
newFaviconElement.href = '/browser-tab.ico';
|
||||
}
|
||||
}
|
||||
|
||||
}, [config?.disableFavicon]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default MetadataProvider;
|
||||
|
@@ -6,7 +6,7 @@ import set from 'lodash/set';
|
||||
import * as React from 'react';
|
||||
|
||||
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||
import { defaultTheme, mationTheme } from 'styles/theme';
|
||||
|
||||
const customizeTheme = (theme, config) => {
|
||||
@@ -28,7 +28,8 @@ const ThemeProvider = ({ children, ...props }) => {
|
||||
const { data: automatischInfo, isPending: isAutomatischInfoPending } =
|
||||
useAutomatischInfo();
|
||||
const isMation = automatischInfo?.data.isMation;
|
||||
const { config, loading: configLoading } = useConfig();
|
||||
const { data: configData, isLoading: configLoading } = useAutomatischConfig();
|
||||
const config = configData?.data;
|
||||
|
||||
const customTheme = React.useMemo(() => {
|
||||
const installationTheme = isMation ? mationTheme : defaultTheme;
|
||||
@@ -51,4 +52,5 @@ const ThemeProvider = ({ children, ...props }) => {
|
||||
</BaseThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeProvider;
|
||||
|
@@ -14,23 +14,23 @@ import EditIcon from '@mui/icons-material/Edit';
|
||||
import TableFooter from '@mui/material/TableFooter';
|
||||
import DeleteUserButton from 'components/DeleteUserButton/index.ee';
|
||||
import ListLoader from 'components/ListLoader';
|
||||
import useUsers from 'hooks/useUsers';
|
||||
import useAdminUsers from 'hooks/useAdminUsers';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import * as URLS from 'config/urls';
|
||||
import TablePaginationActions from './TablePaginationActions';
|
||||
import { TablePagination } from './style';
|
||||
|
||||
export default function UserList() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||
const { users, pageInfo, totalCount, loading } = useUsers(page, rowsPerPage);
|
||||
const { data: usersData, isLoading } = useAdminUsers(page + 1);
|
||||
const users = usersData?.data;
|
||||
const { count } = usersData?.meta || {};
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(+event.target.value);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
@@ -68,14 +68,14 @@ export default function UserList() {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading && (
|
||||
{isLoading && (
|
||||
<ListLoader
|
||||
data-test="users-list-loader"
|
||||
rowsNumber={3}
|
||||
columnsNumber={2}
|
||||
/>
|
||||
)}
|
||||
{!loading &&
|
||||
{!isLoading &&
|
||||
users.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
@@ -120,18 +120,16 @@ export default function UserList() {
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
{totalCount && (
|
||||
{!isLoading && typeof count === 'number' && (
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
data-total-count={totalCount}
|
||||
data-rows-per-page={rowsPerPage}
|
||||
rowsPerPageOptions={[10, 25, 50, 100]}
|
||||
data-total-count={count}
|
||||
rowsPerPageOptions={[]}
|
||||
page={page}
|
||||
count={totalCount}
|
||||
count={count}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
rowsPerPage={10}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
/>
|
||||
</TableRow>
|
||||
|
@@ -1,6 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
export const GET_CONFIG = gql`
|
||||
query GetConfig($keys: [String]) {
|
||||
getConfig(keys: $keys)
|
||||
}
|
||||
`;
|
@@ -1,36 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
export const GET_FLOWS = gql`
|
||||
query GetFlows(
|
||||
$limit: Int!
|
||||
$offset: Int!
|
||||
$appKey: String
|
||||
$connectionId: String
|
||||
$name: String
|
||||
) {
|
||||
getFlows(
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
appKey: $appKey
|
||||
connectionId: $connectionId
|
||||
name: $name
|
||||
) {
|
||||
pageInfo {
|
||||
currentPage
|
||||
totalPages
|
||||
}
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
updatedAt
|
||||
active
|
||||
status
|
||||
steps {
|
||||
iconUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@@ -1,11 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
export const GET_NOTIFICATIONS = gql`
|
||||
query GetNotifications {
|
||||
getNotifications {
|
||||
name
|
||||
createdAt
|
||||
documentationUrl
|
||||
description
|
||||
}
|
||||
}
|
||||
`;
|
@@ -1,23 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
export const GET_USERS = gql`
|
||||
query GetUsers($limit: Int!, $offset: Int!) {
|
||||
getUsers(limit: $limit, offset: $offset) {
|
||||
pageInfo {
|
||||
currentPage
|
||||
totalPages
|
||||
}
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fullName
|
||||
email
|
||||
role {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@@ -1,9 +1,9 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from 'helpers/api';
|
||||
|
||||
export default function useUser({ userId }) {
|
||||
export default function useAdminUser({ userId }) {
|
||||
const query = useQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryKey: ['admin', 'user', userId],
|
||||
queryFn: async ({ signal }) => {
|
||||
const { data } = await api.get(`/v1/admin/users/${userId}`, {
|
||||
signal,
|
17
packages/web/src/hooks/useAdminUsers.js
Normal file
17
packages/web/src/hooks/useAdminUsers.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from 'helpers/api';
|
||||
|
||||
export default function useAdminUsers(page) {
|
||||
const query = useQuery({
|
||||
queryKey: ['admin', 'users', page],
|
||||
queryFn: async ({ signal }) => {
|
||||
const { data } = await api.get(`/v1/admin/users`, {
|
||||
signal,
|
||||
params: { page },
|
||||
});
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
22
packages/web/src/hooks/useAppFlows.js
Normal file
22
packages/web/src/hooks/useAppFlows.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import api from 'helpers/api';
|
||||
|
||||
export default function useAppFlows({ appKey, page }, { enabled }) {
|
||||
const query = useQuery({
|
||||
queryKey: ['appFlows', appKey, page],
|
||||
queryFn: async ({ signal }) => {
|
||||
const { data } = await api.get(`/v1/apps/${appKey}/flows`, {
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled,
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
16
packages/web/src/hooks/useAutomatischConfig.js
Normal file
16
packages/web/src/hooks/useAutomatischConfig.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from 'helpers/api';
|
||||
|
||||
export default function useAutomatischConfig() {
|
||||
const query = useQuery({
|
||||
queryKey: ['automatisch', 'config'],
|
||||
queryFn: async ({ signal }) => {
|
||||
const { data } = await api.get(`/v1/automatisch/config`, {
|
||||
signal,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
16
packages/web/src/hooks/useAutomatischNotifications.js
Normal file
16
packages/web/src/hooks/useAutomatischNotifications.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from 'helpers/api';
|
||||
|
||||
export default function useAutomatischNotifications() {
|
||||
const query = useQuery({
|
||||
queryKey: ['automatisch', 'notifications'],
|
||||
queryFn: async ({ signal }) => {
|
||||
const { data } = await api.get(`/v1/automatisch/notifications`, {
|
||||
signal,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_CONFIG } from 'graphql/queries/get-config.ee';
|
||||
export default function useConfig(keys) {
|
||||
const { data, loading } = useQuery(GET_CONFIG, {
|
||||
variables: { keys },
|
||||
});
|
||||
return {
|
||||
config: data?.getConfig,
|
||||
loading,
|
||||
};
|
||||
}
|
25
packages/web/src/hooks/useConnectionFlows.js
Normal file
25
packages/web/src/hooks/useConnectionFlows.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import api from 'helpers/api';
|
||||
|
||||
export default function useConnectionFlows(
|
||||
{ connectionId, page },
|
||||
{ enabled },
|
||||
) {
|
||||
const query = useQuery({
|
||||
queryKey: ['connectionFlows', connectionId, page],
|
||||
queryFn: async ({ signal }) => {
|
||||
const { data } = await api.get(`/v1/connections/${connectionId}/flows`, {
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled,
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
30
packages/web/src/hooks/useLazyFlows.js
Normal file
30
packages/web/src/hooks/useLazyFlows.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import api from 'helpers/api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
export default function useLazyFlows({ flowName, page }, { onSuccess }) {
|
||||
const abortControllerRef = React.useRef(new AbortController());
|
||||
|
||||
React.useEffect(() => {
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
return () => {
|
||||
abortControllerRef.current?.abort();
|
||||
};
|
||||
}, [flowName]);
|
||||
|
||||
const query = useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await api.get('/v1/flows', {
|
||||
params: { name: flowName, page },
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_NOTIFICATIONS } from 'graphql/queries/get-notifications';
|
||||
export default function useNotifications() {
|
||||
const { data, loading } = useQuery(GET_NOTIFICATIONS);
|
||||
const notifications = data?.getNotifications || [];
|
||||
return {
|
||||
loading,
|
||||
notifications,
|
||||
};
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { GET_USERS } from 'graphql/queries/get-users';
|
||||
const getLimitAndOffset = (page, rowsPerPage) => ({
|
||||
limit: rowsPerPage,
|
||||
offset: page * rowsPerPage,
|
||||
});
|
||||
export default function useUsers(page, rowsPerPage) {
|
||||
const { data, loading } = useQuery(GET_USERS, {
|
||||
variables: getLimitAndOffset(page, rowsPerPage),
|
||||
});
|
||||
const users = data?.getUsers.edges.map(({ node }) => node) || [];
|
||||
const pageInfo = data?.getUsers.pageInfo;
|
||||
const totalCount = data?.getUsers.totalCount;
|
||||
return {
|
||||
users,
|
||||
pageInfo,
|
||||
totalCount,
|
||||
loading,
|
||||
};
|
||||
}
|
@@ -1,11 +1,11 @@
|
||||
import { compare } from 'compare-versions';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import useNotifications from 'hooks/useNotifications';
|
||||
import useAutomatischNotifications from 'hooks/useAutomatischNotifications';
|
||||
import api from 'helpers/api';
|
||||
|
||||
export default function useVersion() {
|
||||
const { notifications } = useNotifications();
|
||||
const { data: notificationsData } = useAutomatischNotifications();
|
||||
const { data } = useQuery({
|
||||
queryKey: ['automatischVersion'],
|
||||
queryFn: async ({ signal }) => {
|
||||
@@ -17,6 +17,7 @@ export default function useVersion() {
|
||||
},
|
||||
});
|
||||
const version = data?.data?.version;
|
||||
const notifications = notificationsData?.data || [];
|
||||
|
||||
const newVersionCount = notifications.reduce((count, notification) => {
|
||||
if (!version) return 0;
|
||||
|
@@ -6,6 +6,7 @@ import MuiTextField from '@mui/material/TextField';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import Can from 'components/Can';
|
||||
import Container from 'components/Container';
|
||||
@@ -29,6 +30,7 @@ export default function CreateUser() {
|
||||
const { data, loading: isRolesLoading } = useRoles();
|
||||
const roles = data?.data;
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleUserCreation = async (userData) => {
|
||||
try {
|
||||
@@ -44,7 +46,7 @@ export default function CreateUser() {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||
enqueueSnackbar(formatMessage('createUser.successfullyCreated'), {
|
||||
variant: 'success',
|
||||
persist: true,
|
||||
|
@@ -7,6 +7,7 @@ import MuiTextField from '@mui/material/TextField';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import * as React from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import Can from 'components/Can';
|
||||
import Container from 'components/Container';
|
||||
@@ -18,7 +19,7 @@ import * as URLS from 'config/urls';
|
||||
import { UPDATE_USER } from 'graphql/mutations/update-user.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
import useUser from 'hooks/useUser';
|
||||
import useAdminUser from 'hooks/useAdminUser';
|
||||
|
||||
function generateRoleOptions(roles) {
|
||||
return roles?.map(({ name: label, id: value }) => ({ label, value }));
|
||||
@@ -28,12 +29,13 @@ export default function EditUser() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const [updateUser, { loading }] = useMutation(UPDATE_USER);
|
||||
const { userId } = useParams();
|
||||
const { data: userData, isLoading: isUserLoading } = useUser({ userId });
|
||||
const { data: userData, isLoading: isUserLoading } = useAdminUser({ userId });
|
||||
const user = userData?.data;
|
||||
const { data, isLoading: isRolesLoading } = useRoles();
|
||||
const roles = data?.data;
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleUserUpdate = async (userDataToUpdate) => {
|
||||
try {
|
||||
@@ -49,6 +51,8 @@ export default function EditUser() {
|
||||
},
|
||||
},
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'user', userId] });
|
||||
|
||||
enqueueSnackbar(formatMessage('editUser.successfullyUpdated'), {
|
||||
variant: 'success',
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import debounce from 'lodash/debounce';
|
||||
import Box from '@mui/material/Box';
|
||||
import Grid from '@mui/material/Grid';
|
||||
@@ -9,6 +8,7 @@ import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Pagination from '@mui/material/Pagination';
|
||||
import PaginationItem from '@mui/material/PaginationItem';
|
||||
|
||||
import Can from 'components/Can';
|
||||
import FlowRow from 'components/FlowRow';
|
||||
import NoResultFound from 'components/NoResultFound';
|
||||
@@ -17,45 +17,37 @@ import Container from 'components/Container';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import SearchInput from 'components/SearchInput';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { GET_FLOWS } from 'graphql/queries/get-flows';
|
||||
import * as URLS from 'config/urls';
|
||||
const FLOW_PER_PAGE = 10;
|
||||
const getLimitAndOffset = (page) => ({
|
||||
limit: FLOW_PER_PAGE,
|
||||
offset: (page - 1) * FLOW_PER_PAGE,
|
||||
});
|
||||
import useLazyFlows from 'hooks/useLazyFlows';
|
||||
|
||||
export default function Flows() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||
const [flowName, setFlowName] = React.useState('');
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [getFlows, { data }] = useLazyQuery(GET_FLOWS, {
|
||||
onCompleted: () => {
|
||||
setLoading(false);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const { data, mutate } = useLazyFlows(
|
||||
{ flowName, page },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setIsLoading(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
const fetchData = React.useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
(name) =>
|
||||
getFlows({
|
||||
variables: {
|
||||
...getLimitAndOffset(page),
|
||||
name,
|
||||
},
|
||||
}),
|
||||
300,
|
||||
),
|
||||
[page, getFlows],
|
||||
);
|
||||
React.useEffect(
|
||||
function fetchFlowsOnSearch() {
|
||||
setLoading(true);
|
||||
fetchData(flowName);
|
||||
},
|
||||
[fetchData, flowName],
|
||||
);
|
||||
|
||||
const fetchData = React.useMemo(() => debounce(mutate, 300), [mutate]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
fetchData({ flowName, page });
|
||||
|
||||
return () => {
|
||||
fetchData.cancel();
|
||||
};
|
||||
}, [fetchData, flowName, page]);
|
||||
|
||||
React.useEffect(
|
||||
function resetPageOnSearch() {
|
||||
// reset search params which only consists of `page`
|
||||
@@ -63,17 +55,15 @@ export default function Flows() {
|
||||
},
|
||||
[flowName],
|
||||
);
|
||||
React.useEffect(function cancelDebounceOnUnmount() {
|
||||
return () => {
|
||||
fetchData.cancel();
|
||||
};
|
||||
}, []);
|
||||
const { pageInfo, edges } = data?.getFlows || {};
|
||||
const flows = edges?.map(({ node }) => node);
|
||||
|
||||
const flows = data?.data || [];
|
||||
const pageInfo = data?.meta;
|
||||
const hasFlows = flows?.length;
|
||||
|
||||
const onSearchChange = React.useCallback((event) => {
|
||||
setFlowName(event.target.value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 3 }}>
|
||||
<Container>
|
||||
@@ -116,18 +106,18 @@ export default function Flows() {
|
||||
</Grid>
|
||||
|
||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||
{loading && (
|
||||
{isLoading && (
|
||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||
)}
|
||||
{!loading &&
|
||||
{!isLoading &&
|
||||
flows?.map((flow) => <FlowRow key={flow.id} flow={flow} />)}
|
||||
{!loading && !hasFlows && (
|
||||
{!isLoading && !hasFlows && (
|
||||
<NoResultFound
|
||||
text={formatMessage('flows.noFlows')}
|
||||
to={URLS.CREATE_FLOW}
|
||||
/>
|
||||
)}
|
||||
{!loading && pageInfo && pageInfo.totalPages > 1 && (
|
||||
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
|
||||
<Pagination
|
||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||
page={pageInfo?.currentPage}
|
||||
|
@@ -9,14 +9,15 @@ import PageTitle from 'components/PageTitle';
|
||||
import * as URLS from 'config/urls';
|
||||
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useNotifications from 'hooks/useNotifications';
|
||||
import useAutomatischNotifications from 'hooks/useAutomatischNotifications';
|
||||
|
||||
export default function Updates() {
|
||||
const navigate = useNavigate();
|
||||
const formatMessage = useFormatMessage();
|
||||
const { notifications } = useNotifications();
|
||||
const { data: notificationsData } = useAutomatischNotifications();
|
||||
const { data: automatischInfo, isPending } = useAutomatischInfo();
|
||||
const isMation = automatischInfo?.data.isMation;
|
||||
const notifications = notificationsData?.data || [];
|
||||
|
||||
React.useEffect(
|
||||
function redirectToHomepageInMation() {
|
||||
|
@@ -3,43 +3,44 @@ import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Skeleton from '@mui/material/Skeleton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import merge from 'lodash/merge';
|
||||
import * as React from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import ColorInput from 'components/ColorInput';
|
||||
import Container from 'components/Container';
|
||||
import Form from 'components/Form';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import TextField from 'components/TextField';
|
||||
import { UPDATE_CONFIG } from 'graphql/mutations/update-config.ee';
|
||||
import { GET_CONFIG } from 'graphql/queries/get-config.ee';
|
||||
import nestObject from 'helpers/nestObject';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import {
|
||||
primaryDarkColor,
|
||||
primaryLightColor,
|
||||
primaryMainColor,
|
||||
} from 'styles/theme';
|
||||
|
||||
const getPrimaryMainColor = (color) => color || primaryMainColor;
|
||||
const getPrimaryDarkColor = (color) => color || primaryDarkColor;
|
||||
const getPrimaryLightColor = (color) => color || primaryLightColor;
|
||||
|
||||
const defaultValues = {
|
||||
title: 'Automatisch',
|
||||
'palette.primary.main': primaryMainColor,
|
||||
'palette.primary.dark': primaryDarkColor,
|
||||
'palette.primary.light': primaryLightColor,
|
||||
};
|
||||
|
||||
export default function UserInterface() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const [updateConfig, { loading }] = useMutation(UPDATE_CONFIG);
|
||||
const { config, loading: configLoading } = useConfig([
|
||||
'title',
|
||||
'palette.primary.main',
|
||||
'palette.primary.light',
|
||||
'palette.primary.dark',
|
||||
'logo.svgData',
|
||||
]);
|
||||
const { data: configData, isLoading: configLoading } = useAutomatischConfig();
|
||||
const config = configData?.data;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const configWithDefaults = merge({}, defaultValues, nestObject(config));
|
||||
const handleUserInterfaceUpdate = async (uiData) => {
|
||||
@@ -64,37 +65,9 @@ export default function UserInterface() {
|
||||
optimisticResponse: {
|
||||
updateConfig: input,
|
||||
},
|
||||
update: async function (cache, { data: { updateConfig } }) {
|
||||
const newConfigWithDefaults = merge({}, defaultValues, updateConfig);
|
||||
cache.writeQuery({
|
||||
query: GET_CONFIG,
|
||||
data: {
|
||||
getConfig: newConfigWithDefaults,
|
||||
},
|
||||
});
|
||||
cache.writeQuery({
|
||||
query: GET_CONFIG,
|
||||
data: {
|
||||
getConfig: newConfigWithDefaults,
|
||||
},
|
||||
variables: {
|
||||
keys: ['logo.svgData'],
|
||||
},
|
||||
});
|
||||
cache.writeQuery({
|
||||
query: GET_CONFIG,
|
||||
data: {
|
||||
getConfig: newConfigWithDefaults,
|
||||
},
|
||||
variables: {
|
||||
keys: [
|
||||
'title',
|
||||
'palette.primary.main',
|
||||
'palette.primary.light',
|
||||
'palette.primary.dark',
|
||||
'logo.svgData',
|
||||
],
|
||||
},
|
||||
update: async function () {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['automatisch', 'config'],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Route, Routes as ReactRouterRoutes, Navigate } from 'react-router-dom';
|
||||
|
||||
import Layout from 'components/Layout';
|
||||
import NoResultFound from 'components/NotFound';
|
||||
import PublicLayout from 'components/PublicLayout';
|
||||
@@ -19,11 +20,14 @@ import * as URLS from 'config/urls';
|
||||
import settingsRoutes from './settingsRoutes';
|
||||
import adminSettingsRoutes from './adminSettingsRoutes';
|
||||
import Notifications from 'pages/Notifications';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import useAutomatischConfig from 'hooks/useAutomatischConfig';
|
||||
import useAuthentication from 'hooks/useAuthentication';
|
||||
|
||||
function Routes() {
|
||||
const { config } = useConfig();
|
||||
const { data: configData } = useAutomatischConfig();
|
||||
const { isAuthenticated } = useAuthentication();
|
||||
const config = configData?.data;
|
||||
|
||||
return (
|
||||
<ReactRouterRoutes>
|
||||
<Route
|
||||
@@ -147,4 +151,5 @@ function Routes() {
|
||||
</ReactRouterRoutes>
|
||||
);
|
||||
}
|
||||
|
||||
export default <Routes />;
|
||||
|
11
yarn.lock
11
yarn.lock
@@ -5471,6 +5471,11 @@ basic-auth@^2.0.1, basic-auth@~2.0.1:
|
||||
dependencies:
|
||||
safe-buffer "5.1.2"
|
||||
|
||||
basic-ftp@^5.0.5:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0"
|
||||
integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==
|
||||
|
||||
batch@0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz"
|
||||
@@ -8379,9 +8384,9 @@ fn.name@1.x.x:
|
||||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0:
|
||||
version "1.15.3"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
|
||||
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
|
||||
version "1.15.6"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
|
||||
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
|
||||
|
||||
forever-agent@~0.6.1:
|
||||
version "0.6.1"
|
||||
|
Reference in New Issue
Block a user