Compare commits
14 Commits
shared-con
...
js-in-ts
Author | SHA1 | Date | |
---|---|---|---|
![]() |
321019d36a | ||
![]() |
d070e976b0 | ||
![]() |
0caf6bfabb | ||
![]() |
b842d7938f | ||
![]() |
cebbf84375 | ||
![]() |
8608431490 | ||
![]() |
78ba18b176 | ||
![]() |
f8c30c8526 | ||
![]() |
693c9b85a5 | ||
![]() |
70bb7defd1 | ||
![]() |
160377ca31 | ||
![]() |
2c0ce77a4e | ||
![]() |
77fbb0c9da | ||
![]() |
5971425d23 |
5
.github/workflows/playwright.yml
vendored
5
.github/workflows/playwright.yml
vendored
@@ -4,6 +4,11 @@ on:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/backend/**'
|
||||
- 'packages/e2e-tests/**'
|
||||
- 'packages/web/**'
|
||||
- '!packages/backend/src/apps/**'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
|
@@ -59,8 +59,8 @@
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"knex": "^2.4.0",
|
||||
"libphonenumber-js": "^1.10.48",
|
||||
"knex": "^2.5.1",
|
||||
"lodash.get": "^4.4.2",
|
||||
"luxon": "2.5.2",
|
||||
"memory-cache": "^0.2.0",
|
||||
@@ -69,7 +69,7 @@
|
||||
"node-html-markdown": "^1.3.0",
|
||||
"nodemailer": "6.7.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"objection": "^3.1.1",
|
||||
"objection": "^3.0.0",
|
||||
"passport": "^0.6.0",
|
||||
"pg": "^8.7.1",
|
||||
"php-serialize": "^4.0.2",
|
||||
|
@@ -11,7 +11,7 @@ export default {
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Host name of your Odoo Server',
|
||||
description: 'Host name of your Odoo Server (e.g. sub.domain.com without the protocol)',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
@@ -25,6 +25,27 @@ export default {
|
||||
description: 'Port that the host is running on, defaults to 443 (HTTPS)',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'secure',
|
||||
label: 'Secure',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: 'true',
|
||||
description: 'True if the host communicates via secure protocol.',
|
||||
variables: false,
|
||||
clickToCopy: false,
|
||||
options: [
|
||||
{
|
||||
label: 'True',
|
||||
value: 'true',
|
||||
},
|
||||
{
|
||||
label: 'False',
|
||||
value: 'false',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'databaseName',
|
||||
label: 'Database Name',
|
||||
@@ -40,7 +61,7 @@ export default {
|
||||
key: 'email',
|
||||
label: 'Email Address',
|
||||
type: 'string' as const,
|
||||
requires: true,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
|
@@ -32,8 +32,10 @@ export const asyncMethodCall = async <T = number>($: IGlobalVariable, { method,
|
||||
export const getClient = ($: IGlobalVariable, { path = 'common' }) => {
|
||||
const host = $.auth.data.host as string;
|
||||
const port = Number($.auth.data.port as string);
|
||||
const secure = $.auth.data.secure === 'true';
|
||||
const createClientFunction = secure ? xmlrpc.createSecureClient : xmlrpc.createClient;
|
||||
|
||||
return xmlrpc.createClient(
|
||||
return createClientFunction(
|
||||
{
|
||||
host,
|
||||
port,
|
||||
|
@@ -49,6 +49,7 @@ type AppConfig = {
|
||||
smtpPassword: string;
|
||||
fromEmail: string;
|
||||
isCloud: boolean;
|
||||
isMation: boolean;
|
||||
isSelfHosted: boolean;
|
||||
paddleVendorId: number;
|
||||
paddleVendorAuthCode: string;
|
||||
@@ -127,6 +128,7 @@ const appConfig: AppConfig = {
|
||||
fromEmail: process.env.FROM_EMAIL,
|
||||
isCloud: process.env.AUTOMATISCH_CLOUD === 'true',
|
||||
isSelfHosted: process.env.AUTOMATISCH_CLOUD !== 'true',
|
||||
isMation: process.env.MATION === 'true',
|
||||
paddleVendorId: Number(process.env.PADDLE_VENDOR_ID),
|
||||
paddleVendorAuthCode: process.env.PADDLE_VENDOR_AUTH_CODE,
|
||||
paddlePublicKey: process.env.PADDLE_PUBLIC_KEY,
|
||||
|
@@ -1,15 +0,0 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.createTable('shared_connections', (table) => {
|
||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||
table.uuid('connection_id').notNullable().references('id').inTable('connections');
|
||||
table.uuid('role_id').notNullable().references('id').inTable('roles');
|
||||
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.dropTable('shared_connections');
|
||||
}
|
@@ -19,7 +19,6 @@ import login from './mutations/login';
|
||||
import registerUser from './mutations/register-user.ee';
|
||||
import resetConnection from './mutations/reset-connection';
|
||||
import resetPassword from './mutations/reset-password.ee';
|
||||
import shareConnection from './mutations/share-connection.ee';
|
||||
import updateAppAuthClient from './mutations/update-app-auth-client.ee';
|
||||
import updateAppConfig from './mutations/update-app-config.ee';
|
||||
import updateConfig from './mutations/update-config.ee';
|
||||
@@ -56,7 +55,6 @@ const mutationResolvers = {
|
||||
registerUser,
|
||||
resetConnection,
|
||||
resetPassword,
|
||||
shareConnection,
|
||||
updateAppAuthClient,
|
||||
updateAppConfig,
|
||||
updateConfig,
|
||||
|
@@ -28,11 +28,11 @@ const createFlow = async (
|
||||
});
|
||||
|
||||
if (connectionId) {
|
||||
const connection = await context.currentUser
|
||||
.relatedConnectionsQuery()
|
||||
const hasConnection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findById(connectionId);
|
||||
|
||||
if (!connection) {
|
||||
if (!hasConnection) {
|
||||
throw new Error('The connection does not exist!');
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Connection from '../../models/connection';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
@@ -12,13 +11,10 @@ const deleteConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('delete', 'Connection');
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const baseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
context.currentUser.can('delete', 'Connection');
|
||||
|
||||
await baseQuery
|
||||
.clone()
|
||||
await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.delete()
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
|
@@ -4,7 +4,6 @@ import deleteUserQueue from '../../queues/delete-user.ee';
|
||||
import flowQueue from '../../queues/flow';
|
||||
import Flow from '../../models/flow';
|
||||
import Execution from '../../models/execution';
|
||||
import User from '../../models/user';
|
||||
import ExecutionStep from '../../models/execution-step';
|
||||
import appConfig from '../../config/app';
|
||||
|
||||
@@ -15,87 +14,51 @@ const deleteCurrentUser = async (
|
||||
) => {
|
||||
const id = context.currentUser.id;
|
||||
|
||||
try {
|
||||
await User.transaction(async (trx) => {
|
||||
const flows = await context.currentUser
|
||||
.$relatedQuery('flows', trx)
|
||||
.where({
|
||||
active: true,
|
||||
});
|
||||
const flows = await context.currentUser.$relatedQuery('flows').where({
|
||||
active: true,
|
||||
});
|
||||
|
||||
const { count } = await context.currentUser
|
||||
.$relatedQuery('connections', trx)
|
||||
.joinRelated('sharedConnections')
|
||||
.joinRelated('steps')
|
||||
.join('flows', function () {
|
||||
this
|
||||
.on(
|
||||
'flows.id', '=', 'steps.flow_id'
|
||||
)
|
||||
.andOnVal(
|
||||
'flows.user_id', '<>', id
|
||||
)
|
||||
.andOnVal(
|
||||
'flows.active', '=', true
|
||||
)
|
||||
})
|
||||
.count()
|
||||
.first();
|
||||
const repeatableJobs = await flowQueue.getRepeatableJobs();
|
||||
|
||||
if (count) {
|
||||
throw new Error('The shared connections must be removed first!');
|
||||
}
|
||||
for (const flow of flows) {
|
||||
const job = repeatableJobs.find((job) => job.id === flow.id);
|
||||
|
||||
const executionIds = (
|
||||
await context.currentUser
|
||||
.$relatedQuery('executions', trx)
|
||||
.select('executions.id')
|
||||
).map((execution: Execution) => execution.id);
|
||||
const flowIds = flows.map((flow) => flow.id);
|
||||
|
||||
await ExecutionStep.query(trx).delete().whereIn('execution_id', executionIds);
|
||||
await context.currentUser.$relatedQuery('executions', trx).delete();
|
||||
await context.currentUser.$relatedQuery('steps', trx).delete();
|
||||
await Flow.query(trx).whereIn('id', flowIds).delete();
|
||||
await context.currentUser.$relatedQuery('connections', trx).delete();
|
||||
await context.currentUser.$relatedQuery('identities', trx).delete();
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
await context.currentUser.$relatedQuery('subscriptions', trx).delete();
|
||||
await context.currentUser.$relatedQuery('usageData', trx).delete();
|
||||
}
|
||||
|
||||
await context.currentUser.$query(trx).delete();
|
||||
|
||||
const jobName = `Delete user - ${id}`;
|
||||
const jobPayload = { id };
|
||||
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
|
||||
const jobOptions = {
|
||||
delay: millisecondsFor30Days,
|
||||
};
|
||||
|
||||
// must be done as the last action as this cannot be reverted via the transaction!
|
||||
const repeatableJobs = await flowQueue.getRepeatableJobs();
|
||||
|
||||
for (const flow of flows) {
|
||||
const job = repeatableJobs.find((job) => job.id === flow.id);
|
||||
|
||||
if (job) {
|
||||
await flowQueue.removeRepeatableByKey(job.key);
|
||||
}
|
||||
}
|
||||
|
||||
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw err;
|
||||
if (job) {
|
||||
await flowQueue.removeRepeatableByKey(job.key);
|
||||
}
|
||||
|
||||
throw new Error('The user deletion has failed!');
|
||||
}
|
||||
|
||||
const executionIds = (
|
||||
await context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
.select('executions.id')
|
||||
).map((execution: Execution) => execution.id);
|
||||
const flowIds = flows.map((flow) => flow.id);
|
||||
|
||||
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
|
||||
await context.currentUser.$relatedQuery('executions').delete();
|
||||
await context.currentUser.$relatedQuery('steps').delete();
|
||||
await Flow.query().whereIn('id', flowIds).delete();
|
||||
await context.currentUser.$relatedQuery('connections').delete();
|
||||
await context.currentUser.$relatedQuery('identities').delete();
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
await context.currentUser.$relatedQuery('subscriptions').delete();
|
||||
await context.currentUser.$relatedQuery('usageData').delete();
|
||||
}
|
||||
|
||||
await context.currentUser.$query().delete();
|
||||
|
||||
const jobName = `Delete user - ${id}`;
|
||||
const jobPayload = { id };
|
||||
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
|
||||
const jobOptions = {
|
||||
delay: millisecondsFor30Days,
|
||||
};
|
||||
|
||||
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default deleteCurrentUser;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Connection from '../../models/connection';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
@@ -12,13 +11,10 @@ const resetConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('update', 'Connection');
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const baseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
let connection = await baseQuery
|
||||
.clone()
|
||||
let connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
})
|
||||
|
@@ -1,55 +0,0 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Connection from '../../models/connection';
|
||||
import SharedConnection from '../../models/shared-connection';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
id: string;
|
||||
roleIds: string[];
|
||||
};
|
||||
};
|
||||
|
||||
const shareConnection = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('update', 'Connection');
|
||||
|
||||
if (conditions.isCreator) return;
|
||||
|
||||
const {
|
||||
id,
|
||||
roleIds,
|
||||
} = params.input;
|
||||
|
||||
const connection = await Connection
|
||||
.query()
|
||||
.findById(id)
|
||||
.throwIfNotFound();
|
||||
|
||||
try {
|
||||
const updatedConnection = await Connection.transaction(async (trx) => {
|
||||
await connection.$relatedQuery('sharedConnections', trx).delete();
|
||||
|
||||
if (roleIds?.length) {
|
||||
const sharedConnections = roleIds.map((roleId) => ({
|
||||
roleId,
|
||||
connectionId: connection.id,
|
||||
}));
|
||||
|
||||
await SharedConnection.query().insert(sharedConnections);
|
||||
}
|
||||
|
||||
return await Connection
|
||||
.query(trx)
|
||||
.findById(id);
|
||||
});
|
||||
|
||||
return updatedConnection;
|
||||
} catch (err) {
|
||||
throw new Error('The connection sharing preferences could not be updated!');
|
||||
}
|
||||
};
|
||||
|
||||
export default shareConnection;
|
@@ -1,7 +1,6 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import Context from '../../types/express/context';
|
||||
import AppAuthClient from '../../models/app-auth-client';
|
||||
import Connection from '../../models/connection';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
@@ -16,13 +15,10 @@ const updateConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('update', 'Connection');
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const baseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
context.currentUser.can('create', 'Connection');
|
||||
|
||||
let connection = await baseQuery
|
||||
.clone()
|
||||
let connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
})
|
||||
|
@@ -45,11 +45,10 @@ const updateStep = async (
|
||||
|
||||
canSeeAllConnections = !conditions.isCreator;
|
||||
} catch {
|
||||
// The user does not have permission to read any connections!
|
||||
throw new Error('The connection does not exist!');
|
||||
// void
|
||||
}
|
||||
|
||||
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const baseConnectionsQuery = canSeeAllConnections ? allConnections : userConnections;
|
||||
|
||||
|
@@ -9,55 +9,28 @@ type Params = {
|
||||
const getApp = async (_parent: unknown, params: Params, context: Context) => {
|
||||
const conditions = context.currentUser.can('read', 'Connection');
|
||||
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
|
||||
const app = await App.findOneByKey(params.key);
|
||||
|
||||
if (context.currentUser) {
|
||||
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||
const allConnections = Connection.query();
|
||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
|
||||
const connections = await Connection.query()
|
||||
.with('connections', connectionBaseQuery)
|
||||
.with(
|
||||
'connections_with_flow_count',
|
||||
Connection.query()
|
||||
.clearSelect()
|
||||
.select('connections.id')
|
||||
.leftJoinRelated('steps')
|
||||
.leftJoin('flows', function () {
|
||||
this
|
||||
.on(
|
||||
'flows.id',
|
||||
'=',
|
||||
'steps.flow_id',
|
||||
)
|
||||
|
||||
if (conditions.isCreator) {
|
||||
this.andOnVal(
|
||||
'flows.user_id',
|
||||
'=',
|
||||
context.currentUser.id
|
||||
)
|
||||
}
|
||||
})
|
||||
.where({
|
||||
'connections.key': params.key,
|
||||
'connections.draft': false,
|
||||
})
|
||||
.countDistinct('steps.flow_id as flowCount')
|
||||
.groupBy('connections.id')
|
||||
)
|
||||
.select(
|
||||
'connections.*',
|
||||
'connections_with_flow_count.flowCount as flowCount'
|
||||
)
|
||||
.from('connections')
|
||||
const connections = await connectionBaseQuery
|
||||
.clone()
|
||||
.select('connections.*')
|
||||
.withGraphFetched({
|
||||
appConfig: true,
|
||||
appAuthClient: true
|
||||
})
|
||||
.joinRaw('join connections_with_flow_count on connections.id = connections_with_flow_count.id')
|
||||
.orderBy('connections.created_at', 'desc');
|
||||
.fullOuterJoinRelated('steps')
|
||||
.where({
|
||||
'connections.key': params.key,
|
||||
'connections.draft': false,
|
||||
})
|
||||
.countDistinct('steps.flow_id as flowCount')
|
||||
.groupBy('connections.id')
|
||||
.orderBy('created_at', 'desc');
|
||||
|
||||
return {
|
||||
...app,
|
||||
|
@@ -9,6 +9,7 @@ describe('graphQL getAutomatischInfo query', () => {
|
||||
query {
|
||||
getAutomatischInfo {
|
||||
isCloud
|
||||
isMation
|
||||
license {
|
||||
id
|
||||
name
|
||||
@@ -24,6 +25,7 @@ describe('graphQL getAutomatischInfo query', () => {
|
||||
jest.spyOn(license, 'getLicense').mockResolvedValue(false);
|
||||
|
||||
jest.replaceProperty(appConfig, 'isCloud', false);
|
||||
jest.replaceProperty(appConfig, 'isMation', false);
|
||||
});
|
||||
|
||||
it('should return empty license data', async () => {
|
||||
@@ -36,6 +38,7 @@ describe('graphQL getAutomatischInfo query', () => {
|
||||
data: {
|
||||
getAutomatischInfo: {
|
||||
isCloud: false,
|
||||
isMation: false,
|
||||
license: {
|
||||
id: null,
|
||||
name: null,
|
||||
@@ -77,6 +80,7 @@ describe('graphQL getAutomatischInfo query', () => {
|
||||
data: {
|
||||
getAutomatischInfo: {
|
||||
isCloud: true,
|
||||
isMation: false,
|
||||
license: {
|
||||
expireAt: '2025-08-09T10:56:54.144Z',
|
||||
id: '123123',
|
||||
@@ -105,6 +109,69 @@ describe('graphQL getAutomatischInfo query', () => {
|
||||
const expectedResponsePayload = {
|
||||
data: {
|
||||
getAutomatischInfo: {
|
||||
isCloud: false,
|
||||
isMation: false,
|
||||
license: {
|
||||
expireAt: '2025-08-09T10:56:54.144Z',
|
||||
id: '123123',
|
||||
name: 'Test License',
|
||||
verified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(response.body).toEqual(expectedResponsePayload);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with mation flag enabled', () => {
|
||||
beforeEach(async () => {
|
||||
jest.replaceProperty(appConfig, 'isCloud', false);
|
||||
jest.replaceProperty(appConfig, 'isMation', true);
|
||||
});
|
||||
|
||||
it('should return all license data', async () => {
|
||||
const response = await request(app)
|
||||
.post('/graphql')
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
|
||||
const expectedResponsePayload = {
|
||||
data: {
|
||||
getAutomatischInfo: {
|
||||
isCloud: false,
|
||||
isMation: true,
|
||||
license: {
|
||||
expireAt: '2025-08-09T10:56:54.144Z',
|
||||
id: '123123',
|
||||
name: 'Test License',
|
||||
verified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(response.body).toEqual(expectedResponsePayload);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and with mation flag disabled', () => {
|
||||
beforeEach(async () => {
|
||||
jest.replaceProperty(appConfig, 'isCloud', false);
|
||||
jest.replaceProperty(appConfig, 'isMation', false);
|
||||
});
|
||||
|
||||
it('should return all license data', async () => {
|
||||
const response = await request(app)
|
||||
.post('/graphql')
|
||||
.send({ query })
|
||||
.expect(200);
|
||||
|
||||
const expectedResponsePayload = {
|
||||
data: {
|
||||
getAutomatischInfo: {
|
||||
isMation: false,
|
||||
isCloud: false,
|
||||
license: {
|
||||
expireAt: '2025-08-09T10:56:54.144Z',
|
||||
|
@@ -13,6 +13,7 @@ const getAutomatischInfo = async () => {
|
||||
|
||||
return {
|
||||
isCloud: appConfig.isCloud,
|
||||
isMation: appConfig.isMation,
|
||||
license: computedLicense,
|
||||
};
|
||||
};
|
||||
|
@@ -15,7 +15,7 @@ const getConnectedApps = async (
|
||||
) => {
|
||||
const conditions = context.currentUser.can('read', 'Connection');
|
||||
|
||||
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
|
||||
@@ -25,9 +25,8 @@ const getConnectedApps = async (
|
||||
|
||||
let apps = await App.findAll(params.name);
|
||||
|
||||
const connections = await Connection
|
||||
.query()
|
||||
.with('connections', connectionBaseQuery)
|
||||
const connections = await connectionBaseQuery
|
||||
.clone()
|
||||
.select('connections.key')
|
||||
.where({ draft: false })
|
||||
.count('connections.id as count')
|
||||
|
@@ -1,29 +0,0 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Connection from '../../models/connection';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const getSharedConnectionRoleIds = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('update', 'Connection');
|
||||
|
||||
if (conditions.isCreator) return;
|
||||
|
||||
const connection = await Connection
|
||||
.query()
|
||||
.findById(params.id)
|
||||
.throwIfNotFound();
|
||||
|
||||
const sharedConnections = await connection.$relatedQuery('sharedConnections');
|
||||
|
||||
const roleIds = sharedConnections.map(({ roleId }) => roleId);
|
||||
|
||||
return roleIds;
|
||||
};
|
||||
|
||||
export default getSharedConnectionRoleIds;
|
10
packages/backend/src/graphql/queries/get-use-js-file.js
Normal file
10
packages/backend/src/graphql/queries/get-use-js-file.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import appConfig from '../../config/app';
|
||||
|
||||
const getUseJsFile = async () => {
|
||||
return {
|
||||
canInvoke: true,
|
||||
appConfig,
|
||||
};
|
||||
};
|
||||
|
||||
export default getUseJsFile;
|
@@ -13,15 +13,15 @@ const testConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('read', 'Connection');
|
||||
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||
const conditions = context.currentUser.can('update', 'Connection');
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
|
||||
let connection = await connectionBaseQuery
|
||||
.clone()
|
||||
.findOne({
|
||||
'connections.id': params.id,
|
||||
id: params.id,
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import getApps from './queries/get-apps';
|
||||
import getAutomatischInfo from './queries/get-automatisch-info';
|
||||
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
|
||||
import getConfig from './queries/get-config.ee';
|
||||
import getUseJsFile from './queries/get-use-js-file.js';
|
||||
import getConnectedApps from './queries/get-connected-apps';
|
||||
import getCurrentUser from './queries/get-current-user';
|
||||
import getDynamicData from './queries/get-dynamic-data';
|
||||
@@ -24,7 +25,6 @@ import getRole from './queries/get-role.ee';
|
||||
import getRoles from './queries/get-roles.ee';
|
||||
import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee';
|
||||
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee';
|
||||
import getSharedConnectionRoleIds from './queries/get-shared-connection-role-ids.ee';
|
||||
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
|
||||
import getSubscriptionStatus from './queries/get-subscription-status.ee';
|
||||
import getTrialStatus from './queries/get-trial-status.ee';
|
||||
@@ -61,7 +61,6 @@ const queryResolvers = {
|
||||
getRoles,
|
||||
getSamlAuthProvider,
|
||||
getSamlAuthProviderRoleMappings,
|
||||
getSharedConnectionRoleIds,
|
||||
getStepWithTestExecutions,
|
||||
getSubscriptionStatus,
|
||||
getTrialStatus,
|
||||
@@ -70,6 +69,7 @@ const queryResolvers = {
|
||||
healthcheck,
|
||||
listSamlAuthProviders,
|
||||
testConnection,
|
||||
getUseJsFile,
|
||||
};
|
||||
|
||||
export default queryResolvers;
|
||||
|
@@ -11,6 +11,7 @@ type Query {
|
||||
getConnectedApps(name: String): [App]
|
||||
testConnection(id: String!): Connection
|
||||
getFlow(id: String!): Flow
|
||||
getUseJsFile: JSONObject
|
||||
getFlows(
|
||||
limit: Int!
|
||||
offset: Int!
|
||||
@@ -53,7 +54,6 @@ type Query {
|
||||
getNotifications: [Notification]
|
||||
getSamlAuthProvider: SamlAuthProvider
|
||||
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]
|
||||
getSharedConnectionRoleIds(id: String!): [String]
|
||||
getSubscriptionStatus: GetSubscriptionStatus
|
||||
getTrialStatus: GetTrialStatus
|
||||
getUser(id: String!): User
|
||||
@@ -84,7 +84,6 @@ type Mutation {
|
||||
registerUser(input: RegisterUserInput): User
|
||||
resetConnection(input: ResetConnectionInput): Connection
|
||||
resetPassword(input: ResetPasswordInput): Boolean
|
||||
shareConnection(input: ShareConnectionInput): Connection
|
||||
updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient
|
||||
updateAppConfig(input: UpdateAppConfigInput): AppConfig
|
||||
updateConfig(input: JSONObject): JSONObject
|
||||
@@ -246,7 +245,6 @@ type AuthLink {
|
||||
type Connection {
|
||||
id: String
|
||||
key: String
|
||||
shared: Boolean
|
||||
reconnectable: Boolean
|
||||
appAuthClientId: String
|
||||
formattedData: ConnectionData
|
||||
@@ -649,6 +647,7 @@ type AppHealth {
|
||||
|
||||
type GetAutomatischInfo {
|
||||
isCloud: Boolean
|
||||
isMation: Boolean
|
||||
license: License
|
||||
}
|
||||
|
||||
@@ -813,11 +812,6 @@ input ExecutionFiltersInput {
|
||||
status: String
|
||||
}
|
||||
|
||||
input ShareConnectionInput {
|
||||
id: String!
|
||||
roleIds: [String]
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import { IJSONObject, IRequest } from '@automatisch/types';
|
||||
import { AES, enc } from 'crypto-js';
|
||||
import { QueryContext, ModelOptions } from 'objection';
|
||||
import type { RelationMappings } from 'objection';
|
||||
import { ModelOptions, QueryContext } from 'objection';
|
||||
import appConfig from '../config/app';
|
||||
import globalVariable from '../helpers/global-variable';
|
||||
import Telemetry from '../helpers/telemetry';
|
||||
import { AES, enc } from 'crypto-js';
|
||||
import { IRequest } from '@automatisch/types';
|
||||
import App from './app';
|
||||
import AppAuthClient from './app-auth-client';
|
||||
import AppConfig from './app-config';
|
||||
import AppAuthClient from './app-auth-client';
|
||||
import Base from './base';
|
||||
import ExtendedQueryBuilder from './query-builder';
|
||||
import SharedConnection from './shared-connection';
|
||||
import Step from './step';
|
||||
import User from './user';
|
||||
import Step from './step';
|
||||
import ExtendedQueryBuilder from './query-builder';
|
||||
import appConfig from '../config/app';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import Telemetry from '../helpers/telemetry';
|
||||
import globalVariable from '../helpers/global-variable';
|
||||
|
||||
class Connection extends Base {
|
||||
id!: string;
|
||||
@@ -24,9 +24,6 @@ class Connection extends Base {
|
||||
draft: boolean;
|
||||
count?: number;
|
||||
flowCount?: number;
|
||||
sharedConnections?: SharedConnection[];
|
||||
// computed via `User.relevantConnectionsQuery`
|
||||
shared?: boolean;
|
||||
user?: User;
|
||||
steps?: Step[];
|
||||
triggerSteps?: Step[];
|
||||
@@ -49,7 +46,6 @@ class Connection extends Base {
|
||||
appAuthClientId: { type: 'string', format: 'uuid' },
|
||||
verified: { type: 'boolean', default: false },
|
||||
draft: { type: 'boolean' },
|
||||
shared: { type: 'boolean', readOnly: true, },
|
||||
deletedAt: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
@@ -104,14 +100,6 @@ class Connection extends Base {
|
||||
to: 'app_auth_clients.id',
|
||||
},
|
||||
},
|
||||
sharedConnections: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: SharedConnection,
|
||||
join: {
|
||||
from: 'connections.id',
|
||||
to: 'shared_connections.connection_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
get reconnectable() {
|
||||
|
@@ -1,45 +0,0 @@
|
||||
import Base from './base';
|
||||
import Role from './role';
|
||||
import User from './user';
|
||||
|
||||
class SharedConnection extends Base {
|
||||
id!: string;
|
||||
roleId!: string;
|
||||
connectionId!: string;
|
||||
|
||||
static tableName = 'shared_connections';
|
||||
|
||||
static jsonSchema = {
|
||||
type: 'object',
|
||||
required: ['roleId', 'connectionId'],
|
||||
|
||||
properties: {
|
||||
id: { type: 'string', format: 'uuid' },
|
||||
roleId: { type: 'string', format: 'uuid' },
|
||||
connectionId: { type: 'string', format: 'uuid' },
|
||||
createdAt: { type: 'string' },
|
||||
updatedAt: { type: 'string' },
|
||||
},
|
||||
};
|
||||
|
||||
static relationMappings = () => ({
|
||||
roles: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: Role,
|
||||
join: {
|
||||
from: 'shared_connections.role_id',
|
||||
to: 'roles.id',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: User,
|
||||
join: {
|
||||
from: 'shared_connections.role_id',
|
||||
to: 'users.role_id',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default SharedConnection;
|
@@ -1,7 +1,7 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import { DateTime } from 'luxon';
|
||||
import crypto from 'node:crypto';
|
||||
import { raw, ModelOptions, QueryContext } from 'objection';
|
||||
import { ModelOptions, QueryContext } from 'objection';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
import { hasValidLicense } from '../helpers/license.ee';
|
||||
@@ -28,7 +28,6 @@ class User extends Base {
|
||||
resetPasswordTokenSentAt: string;
|
||||
trialExpiryDate: string;
|
||||
connections?: Connection[];
|
||||
sharedConnections?: Connection[];
|
||||
flows?: Flow[];
|
||||
steps?: Step[];
|
||||
executions?: Execution[];
|
||||
@@ -70,18 +69,6 @@ class User extends Base {
|
||||
to: 'connections.user_id',
|
||||
},
|
||||
},
|
||||
sharedConnections: {
|
||||
relation: Base.ManyToManyRelation,
|
||||
modelClass: Connection,
|
||||
join: {
|
||||
from: 'users.role_id',
|
||||
through: {
|
||||
from: 'shared_connections.role_id',
|
||||
to: 'shared_connections.connection_id',
|
||||
},
|
||||
to: 'connections.id',
|
||||
},
|
||||
},
|
||||
flows: {
|
||||
relation: Base.HasManyRelation,
|
||||
modelClass: Flow,
|
||||
@@ -178,40 +165,6 @@ class User extends Base {
|
||||
},
|
||||
});
|
||||
|
||||
relatedConnectionsQuery() {
|
||||
return Connection
|
||||
.query()
|
||||
.select('connections.*', raw('shared_connections.role_id IS NOT NULL as shared'))
|
||||
.leftJoin(
|
||||
'shared_connections',
|
||||
'connections.id',
|
||||
'=',
|
||||
'shared_connections.connection_id'
|
||||
)
|
||||
.join(
|
||||
'users',
|
||||
function () {
|
||||
this
|
||||
.on(
|
||||
'users.id',
|
||||
'=',
|
||||
'connections.user_id',
|
||||
)
|
||||
.orOn(
|
||||
'users.role_id',
|
||||
'=',
|
||||
'shared_connections.role_id'
|
||||
)
|
||||
},
|
||||
)
|
||||
.where(
|
||||
'users.id',
|
||||
'=',
|
||||
this.id
|
||||
)
|
||||
.groupBy('connections.id', 'shared_connections.role_id');
|
||||
}
|
||||
|
||||
login(password: string) {
|
||||
return bcrypt.compare(password, this.password);
|
||||
}
|
||||
|
@@ -2,11 +2,12 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2021"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"noImplicitAny": false,
|
||||
"outDir": "dist",
|
||||
"paths": {
|
||||
"*": ["../../node_modules/*", "node_modules/*", "src/types/*"]
|
||||
|
@@ -12,7 +12,9 @@ connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||
1. Enter necessary information in the form.
|
||||
1. Check **Enable OAuth Settings** checkbox.
|
||||
1. Copy **OAuth Redirect URL** from Automatisch and paste it to the **Callback URL** field.
|
||||
1. Add any scopes you plan to use in the **Selected OAuth Scopes** section.
|
||||
1. Add any scopes you plan to use in the **Selected OAuth Scopes** section. We suggest `full` and `refresh_token, offline_access` scopes.
|
||||
1. Uncheck "Require Proof Key for Code Exchange (PKCE) Extension for Supported Authorization Flows" checkbox.
|
||||
1. Check "Enable Authorization Code and Credentials Flow" checkbox
|
||||
1. Click on the **Save** button at the bottom of the page.
|
||||
1. Acknowledge the information and click on the **Continue** button.
|
||||
1. In the **API (Enable OAuth Settings)** section, click the **Manager Consumer Details** button.
|
||||
|
1
packages/types/index.d.ts
vendored
1
packages/types/index.d.ts
vendored
@@ -23,7 +23,6 @@ export interface IConnection {
|
||||
formattedData?: IJSONObject;
|
||||
userId: string;
|
||||
verified: boolean;
|
||||
shared?: boolean;
|
||||
count?: number;
|
||||
flowCount?: number;
|
||||
appData?: IApp;
|
||||
|
@@ -1,168 +0,0 @@
|
||||
import type { IApp, IField, IJSONObject } from '@automatisch/types';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import * as React from 'react';
|
||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
|
||||
import InputCreator from 'components/InputCreator';
|
||||
import * as URLS from 'config/urls';
|
||||
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { generateExternalLink } from '../../helpers/translationValues';
|
||||
import { Form } from './style';
|
||||
|
||||
type AdminApplicationConnectionCreateProps = {
|
||||
onClose: (response: Record<string, unknown>) => void;
|
||||
application: IApp;
|
||||
connectionId?: string;
|
||||
};
|
||||
|
||||
export default function AdminApplicationConnectionCreate(
|
||||
props: AdminApplicationConnectionCreateProps
|
||||
): React.ReactElement {
|
||||
const { application, connectionId, onClose } = props;
|
||||
const { name, authDocUrl, key, auth } = application;
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const formatMessage = useFormatMessage();
|
||||
const [error, setError] = React.useState<IJSONObject | null>(null);
|
||||
const [inProgress, setInProgress] = React.useState(false);
|
||||
const hasConnection = Boolean(connectionId);
|
||||
const useShared = searchParams.get('shared') === 'true';
|
||||
const appAuthClientId = searchParams.get('appAuthClientId') || undefined;
|
||||
const { authenticate } = useAuthenticateApp({
|
||||
appKey: key,
|
||||
connectionId,
|
||||
appAuthClientId,
|
||||
useShared: !!appAuthClientId,
|
||||
});
|
||||
|
||||
React.useEffect(function relayProviderData() {
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({
|
||||
source: 'automatisch',
|
||||
payload: { search: window.location.search, hash: window.location.hash },
|
||||
});
|
||||
window.close();
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(
|
||||
function initiateSharedAuthenticationForGivenAuthClient() {
|
||||
if (!appAuthClientId) return;
|
||||
if (!authenticate) return;
|
||||
|
||||
const asyncAuthenticate = async () => {
|
||||
await authenticate();
|
||||
|
||||
navigate(URLS.ADMIN_APP_CONNECTIONS(key));
|
||||
};
|
||||
|
||||
asyncAuthenticate();
|
||||
},
|
||||
[appAuthClientId, authenticate]
|
||||
);
|
||||
|
||||
const handleClientClick = (appAuthClientId: string) =>
|
||||
navigate(
|
||||
URLS.ADMIN_APP_CONNECTIONS_CREATE_WITH_AUTH_CLIENT_ID(
|
||||
key,
|
||||
appAuthClientId
|
||||
)
|
||||
);
|
||||
|
||||
const handleAuthClientsDialogClose = () =>
|
||||
navigate(URLS.ADMIN_APP_CONNECTIONS(key));
|
||||
|
||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
||||
async (data) => {
|
||||
if (!authenticate) return;
|
||||
|
||||
setInProgress(true);
|
||||
|
||||
try {
|
||||
const response = await authenticate({
|
||||
fields: data,
|
||||
});
|
||||
onClose(response as Record<string, unknown>);
|
||||
} catch (err) {
|
||||
const error = err as IJSONObject;
|
||||
console.log(error);
|
||||
setError((error.graphQLErrors as IJSONObject[])?.[0]);
|
||||
} finally {
|
||||
setInProgress(false);
|
||||
}
|
||||
},
|
||||
[authenticate]
|
||||
);
|
||||
|
||||
if (useShared)
|
||||
return (
|
||||
<AppAuthClientsDialog
|
||||
appKey={key}
|
||||
onClose={handleAuthClientsDialogClose}
|
||||
onClientClick={handleClientClick}
|
||||
/>
|
||||
);
|
||||
|
||||
if (appAuthClientId) return <React.Fragment />;
|
||||
|
||||
return (
|
||||
<Dialog open={true} onClose={onClose}>
|
||||
<DialogTitle>
|
||||
{hasConnection
|
||||
? formatMessage('adminAppsConnections.reconnectConnection')
|
||||
: formatMessage('adminAppsConnections.createConnection')}
|
||||
</DialogTitle>
|
||||
|
||||
{authDocUrl && (
|
||||
<Alert severity="info" sx={{ fontWeight: 300 }}>
|
||||
{formatMessage('adminAppsConnections.callToDocs', {
|
||||
appName: name,
|
||||
docsLink: generateExternalLink(authDocUrl),
|
||||
})}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||
>
|
||||
{error.message}
|
||||
{error.details && (
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(error.details, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<DialogContent>
|
||||
<DialogContentText tabIndex={-1} component="div">
|
||||
<Form onSubmit={submitHandler}>
|
||||
{auth?.fields?.map((field: IField) => (
|
||||
<InputCreator key={field.key} schema={field} />
|
||||
))}
|
||||
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ boxShadow: 2 }}
|
||||
loading={inProgress}
|
||||
>
|
||||
{formatMessage('adminAppsConnections.submit')}
|
||||
</LoadingButton>
|
||||
</Form>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import BaseForm from 'components/Form';
|
||||
|
||||
export const Form = styled(BaseForm)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
paddingTop: theme.spacing(1),
|
||||
}));
|
@@ -1,70 +0,0 @@
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import ControlledCheckbox from 'components/ControlledCheckbox';
|
||||
import { Stack } from '@mui/material';
|
||||
|
||||
type Roles = { id: string; name: string; checked: boolean }[];
|
||||
|
||||
function RolesFieldArray() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { control, watch, setValue } = useFormContext();
|
||||
const fieldArrayData = useFieldArray({
|
||||
control,
|
||||
name: 'roles',
|
||||
});
|
||||
|
||||
const fields = fieldArrayData.fields as Roles;
|
||||
const watchedFields = watch('roles') as Roles;
|
||||
const allFieldsSelected = watchedFields.every((field) => field.checked);
|
||||
const allFieldsDeselected = watchedFields.every((field) => !field.checked);
|
||||
|
||||
const handleSelectAllClick = () => {
|
||||
setValue(
|
||||
'roles',
|
||||
watchedFields.map((field) => ({ ...field, checked: !allFieldsSelected })),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="column" spacing={1}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
color="primary"
|
||||
indeterminate={!(allFieldsSelected || allFieldsDeselected)}
|
||||
checked={allFieldsSelected}
|
||||
onChange={handleSelectAllClick}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
allFieldsSelected
|
||||
? formatMessage('adminAppsConnections.deselectAll')
|
||||
: formatMessage('adminAppsConnections.selectAll')
|
||||
}
|
||||
sx={{ margin: 0 }}
|
||||
/>
|
||||
<Divider />
|
||||
{fields.map((role, index) => {
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={role.id}
|
||||
control={
|
||||
<ControlledCheckbox
|
||||
name={`roles.${index}.checked`}
|
||||
defaultValue={role.checked}
|
||||
/>
|
||||
}
|
||||
label={role.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default RolesFieldArray;
|
@@ -1,140 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { IApp, IRole } from '@automatisch/types';
|
||||
import { FieldValues, SubmitHandler } from 'react-hook-form';
|
||||
|
||||
import { SHARE_CONNECTION } from 'graphql/mutations/share-connection';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useSharedConnectionRoleIds from 'hooks/useSharedConnectionRoleIds';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
|
||||
import RolesFieldArray from './RolesFieldArray';
|
||||
import { Form } from './style';
|
||||
|
||||
type AdminApplicationConnectionShareProps = {
|
||||
onClose: (response: Record<string, unknown>) => void;
|
||||
application: IApp;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
function generateRolesData(roles: IRole[], roleIds: string[]) {
|
||||
return roles.map(({ id, name }) => ({
|
||||
id,
|
||||
name,
|
||||
checked: roleIds.includes(id),
|
||||
}));
|
||||
}
|
||||
|
||||
export default function AdminApplicationConnectionShare(
|
||||
props: AdminApplicationConnectionShareProps
|
||||
): React.ReactElement {
|
||||
const { onClose } = props;
|
||||
const { connectionId } = useParams() as Params;
|
||||
const formatMessage = useFormatMessage();
|
||||
const [
|
||||
shareConnection,
|
||||
{ loading: loadingShareConnection, error: shareConnectionError },
|
||||
] = useMutation(SHARE_CONNECTION, {
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
const {
|
||||
roleIds,
|
||||
loading: roleIdsLoading,
|
||||
error: roleIdsError,
|
||||
} = useSharedConnectionRoleIds(connectionId, {
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
const { roles, loading: rolesLoading, error: rolesError } = useRoles();
|
||||
|
||||
const error = shareConnectionError || roleIdsError || rolesError;
|
||||
const showDialogContent =
|
||||
!roleIdsLoading && !rolesLoading && !roleIdsError && !rolesError;
|
||||
|
||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
||||
async (data) => {
|
||||
const roles = data.roles as {
|
||||
id: string;
|
||||
name: string;
|
||||
checked: boolean;
|
||||
}[];
|
||||
|
||||
const response = await shareConnection({
|
||||
variables: {
|
||||
input: {
|
||||
id: connectionId,
|
||||
roleIds: roles
|
||||
.filter((role) => role.checked)
|
||||
.map((role) => role.id),
|
||||
},
|
||||
},
|
||||
});
|
||||
onClose(response as Record<string, unknown>);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const defaultValues = React.useMemo(
|
||||
() => ({
|
||||
roles: generateRolesData(roles, roleIds),
|
||||
}),
|
||||
[roles, roleIds]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={true} onClose={onClose}>
|
||||
<DialogTitle>
|
||||
{formatMessage('adminAppsConnections.shareConnection')}
|
||||
</DialogTitle>
|
||||
{error && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
||||
>
|
||||
{error.message}
|
||||
</Alert>
|
||||
)}
|
||||
{(roleIdsLoading || rolesLoading) && (
|
||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||
)}
|
||||
{showDialogContent && (
|
||||
<DialogContent sx={{ pt: '0px !important' }}>
|
||||
<DialogContentText tabIndex={-1} component="div">
|
||||
<Form
|
||||
defaultValues={defaultValues}
|
||||
onSubmit={submitHandler}
|
||||
render={({ formState: { isDirty } }) => {
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<RolesFieldArray />
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ boxShadow: 2, mt: 5 }}
|
||||
disabled={!isDirty}
|
||||
loading={loadingShareConnection}
|
||||
>
|
||||
{formatMessage('adminAppsConnections.submit')}
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
);
|
||||
}}
|
||||
></Form>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import BaseForm from 'components/Form';
|
||||
|
||||
export const Form = styled(BaseForm)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
paddingTop: theme.spacing(1),
|
||||
}));
|
@@ -1,85 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import type { PopoverProps } from '@mui/material/Popover';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import type { IConnection } from '@automatisch/types';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
type Action = {
|
||||
type: 'test' | 'reconnect' | 'delete' | 'shareConnection';
|
||||
};
|
||||
|
||||
type ContextMenuProps = {
|
||||
appKey: string;
|
||||
connection: IConnection;
|
||||
onClose: () => void;
|
||||
onMenuItemClick: (event: React.MouseEvent, action: Action) => void;
|
||||
anchorEl: PopoverProps['anchorEl'];
|
||||
disableReconnection: boolean;
|
||||
};
|
||||
|
||||
export default function ContextMenu(
|
||||
props: ContextMenuProps
|
||||
): React.ReactElement {
|
||||
const {
|
||||
appKey,
|
||||
connection,
|
||||
onClose,
|
||||
onMenuItemClick,
|
||||
anchorEl,
|
||||
disableReconnection,
|
||||
} = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const createActionHandler = React.useCallback(
|
||||
(action: Action) => {
|
||||
return function clickHandler(event: React.MouseEvent) {
|
||||
onMenuItemClick(event, action);
|
||||
|
||||
onClose();
|
||||
};
|
||||
},
|
||||
[onMenuItemClick, onClose]
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={true}
|
||||
onClose={onClose}
|
||||
hideBackdrop={false}
|
||||
anchorEl={anchorEl}
|
||||
>
|
||||
<MenuItem onClick={createActionHandler({ type: 'test' })}>
|
||||
{formatMessage('adminAppsConnections.testConnection')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
component={Link}
|
||||
disabled={disableReconnection}
|
||||
to={URLS.ADMIN_APP_RECONNECT_CONNECTION(
|
||||
appKey,
|
||||
connection.id,
|
||||
connection.appAuthClientId
|
||||
)}
|
||||
onClick={createActionHandler({ type: 'reconnect' })}
|
||||
>
|
||||
{formatMessage('adminAppsConnections.reconnect')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={URLS.ADMIN_APP_SHARE_CONNECTION(appKey, connection.id)}
|
||||
onClick={createActionHandler({ type: 'shareConnection' })}
|
||||
>
|
||||
{formatMessage('adminAppsConnections.shareConnection')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={createActionHandler({ type: 'delete' })}>
|
||||
{formatMessage('adminAppsConnections.delete')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
@@ -1,155 +0,0 @@
|
||||
import type { IConnection } from '@automatisch/types';
|
||||
import { useLazyQuery, useMutation } from '@apollo/client';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardActionArea from '@mui/material/CardActionArea';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { DateTime } from 'luxon';
|
||||
import * as React from 'react';
|
||||
|
||||
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
|
||||
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
import ConnectionContextMenu from '../AppConnectionContextMenu';
|
||||
import { CardContent, Typography } from './style';
|
||||
|
||||
type AppConnectionRowProps = {
|
||||
connection: IConnection;
|
||||
};
|
||||
|
||||
function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
const [verificationVisible, setVerificationVisible] = React.useState(false);
|
||||
const [testConnection, { called: testCalled, loading: testLoading }] =
|
||||
useLazyQuery(TEST_CONNECTION, {
|
||||
fetchPolicy: 'network-only',
|
||||
onCompleted: () => {
|
||||
setTimeout(() => setVerificationVisible(false), 3000);
|
||||
},
|
||||
onError: () => {
|
||||
setTimeout(() => setVerificationVisible(false), 3000);
|
||||
},
|
||||
});
|
||||
const [deleteConnection] = useMutation(DELETE_CONNECTION);
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
const { id, key, formattedData, verified, createdAt, reconnectable, shared } =
|
||||
props.connection;
|
||||
|
||||
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
|
||||
const onContextMenuAction = React.useCallback(
|
||||
async (event, action: { [key: string]: string }) => {
|
||||
if (action.type === 'delete') {
|
||||
await deleteConnection({
|
||||
variables: { input: { id } },
|
||||
update: (cache) => {
|
||||
const connectionCacheId = cache.identify({
|
||||
__typename: 'Connection',
|
||||
id,
|
||||
});
|
||||
|
||||
cache.evict({
|
||||
id: connectionCacheId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSnackbar(formatMessage('adminAppsConnections.deletedMessage'), {
|
||||
variant: 'success',
|
||||
});
|
||||
} else if (action.type === 'test') {
|
||||
setVerificationVisible(true);
|
||||
testConnection({ variables: { id } });
|
||||
}
|
||||
},
|
||||
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]
|
||||
);
|
||||
|
||||
const relativeCreatedAt = DateTime.fromMillis(
|
||||
parseInt(createdAt, 10)
|
||||
).toRelative();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card sx={{ my: 2 }}>
|
||||
<CardActionArea onClick={onContextMenuClick}>
|
||||
<CardContent>
|
||||
<Stack justifyContent="center" alignItems="flex-start" spacing={1}>
|
||||
<Typography variant="h6" sx={{ textAlign: 'left' }}>
|
||||
{formattedData?.screenName} {shared && 'shared'}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption">
|
||||
{formatMessage('adminAppsConnections.addedAt', {
|
||||
datetime: relativeCreatedAt,
|
||||
})}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
{verificationVisible && testCalled && testLoading && (
|
||||
<>
|
||||
<CircularProgress size={16} />
|
||||
<Typography variant="caption">
|
||||
{formatMessage('adminAppsConnections.testing')}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{verificationVisible && testCalled && !testLoading && verified && (
|
||||
<>
|
||||
<CheckCircleIcon fontSize="small" color="success" />
|
||||
<Typography variant="caption">
|
||||
{formatMessage('adminAppsConnections.testSuccessful')}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{verificationVisible &&
|
||||
testCalled &&
|
||||
!testLoading &&
|
||||
!verified && (
|
||||
<>
|
||||
<ErrorIcon fontSize="small" color="error" />
|
||||
<Typography variant="caption">
|
||||
{formatMessage('adminAppsConnections.testFailed')}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<MoreHorizIcon ref={contextButtonRef} />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
|
||||
{anchorEl && (
|
||||
<ConnectionContextMenu
|
||||
appKey={key}
|
||||
connection={props.connection}
|
||||
disableReconnection={!reconnectable}
|
||||
onClose={handleClose}
|
||||
onMenuItemClick={onContextMenuAction}
|
||||
anchorEl={anchorEl}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppConnectionRow;
|
@@ -1,16 +0,0 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import MuiCardContent from '@mui/material/CardContent';
|
||||
import MuiTypography from '@mui/material/Typography';
|
||||
|
||||
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto',
|
||||
gridTemplateColumns: '1fr auto auto auto',
|
||||
gridColumnGap: theme.spacing(2),
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
export const Typography = styled(MuiTypography)(() => ({
|
||||
textAlign: 'center',
|
||||
display: 'inline-block',
|
||||
}));
|
@@ -1,55 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Button from '@mui/material/Button';
|
||||
import type { IConnection } from '@automatisch/types';
|
||||
|
||||
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
||||
import * as URLS from 'config/urls';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import NoResultFound from 'components/NoResultFound';
|
||||
|
||||
import AppConnectionRow from './AppConnectionRow';
|
||||
|
||||
type AdminApplicationConnectionsProps = { appKey: string };
|
||||
|
||||
function AdminApplicationConnections(
|
||||
props: AdminApplicationConnectionsProps
|
||||
): React.ReactElement {
|
||||
const { appKey } = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
const { data, loading } = useQuery(GET_APP_CONNECTIONS, {
|
||||
variables: { key: appKey },
|
||||
});
|
||||
const appConnections: IConnection[] = data?.getApp?.connections || [];
|
||||
|
||||
if (loading)
|
||||
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
|
||||
|
||||
if (appConnections.length === 0) {
|
||||
return (
|
||||
<NoResultFound
|
||||
to={URLS.ADMIN_APP_CONNECTIONS_CREATE(appKey)}
|
||||
text={formatMessage('adminAppsConnections.noConnections')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{appConnections.map((appConnection) => (
|
||||
<AppConnectionRow key={appConnection.id} connection={appConnection} />
|
||||
))}
|
||||
<Stack justifyContent="flex-end" direction="row">
|
||||
<Link to={URLS.ADMIN_APP_CONNECTIONS_CREATE(appKey)}>
|
||||
<Button variant="contained" sx={{ mt: 2 }} component="div">
|
||||
{formatMessage('adminAppsConnections.createConnection')}
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminApplicationConnections;
|
@@ -53,7 +53,6 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
createdAt,
|
||||
flowCount,
|
||||
reconnectable,
|
||||
shared,
|
||||
} = props.connection;
|
||||
|
||||
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
|
||||
@@ -106,7 +105,7 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
<CardContent>
|
||||
<Stack justifyContent="center" alignItems="flex-start" spacing={1}>
|
||||
<Typography variant="h6" sx={{ textAlign: 'left' }}>
|
||||
{formattedData?.screenName} {shared && 'shared'}
|
||||
{formattedData?.screenName}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption">
|
||||
|
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
||||
|
||||
export const LogoImage = styled('img')(() => ({
|
||||
maxWidth: 200,
|
||||
maxHeight: 50,
|
||||
maxHeight: 22,
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
}));
|
||||
|
22
packages/web/src/components/DefaultLogo/index.tsx
Normal file
22
packages/web/src/components/DefaultLogo/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import Typography from '@mui/material/Typography';
|
||||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import MationLogo from 'components/MationLogo';
|
||||
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||
|
||||
const DefaultLogo = () => {
|
||||
const { isMation, loading } = useAutomatischInfo();
|
||||
|
||||
if (loading) return <React.Fragment />;
|
||||
|
||||
if (isMation) return <MationLogo />;
|
||||
|
||||
return (
|
||||
<Typography variant="h6" component="h1" data-test="typography-logo" noWrap>
|
||||
<FormattedMessage id="brandText" />
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export default DefaultLogo;
|
@@ -12,6 +12,7 @@ import * as URLS from 'config/urls';
|
||||
import useVersion from 'hooks/useVersion';
|
||||
import AppBar from 'components/AppBar';
|
||||
import Drawer from 'components/Drawer';
|
||||
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||
|
||||
type PublicLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -38,19 +39,36 @@ const drawerLinks = [
|
||||
},
|
||||
];
|
||||
|
||||
const generateDrawerBottomLinks = ({ notificationBadgeContent = 0 }) => [
|
||||
{
|
||||
Icon: NotificationsIcon,
|
||||
primary: 'settingsDrawer.notifications',
|
||||
to: URLS.UPDATES,
|
||||
badgeContent: notificationBadgeContent,
|
||||
},
|
||||
];
|
||||
type GenerateDrawerBottomLinksOptions = {
|
||||
isMation: boolean;
|
||||
loading: boolean;
|
||||
notificationBadgeContent: number;
|
||||
};
|
||||
|
||||
const generateDrawerBottomLinks = ({
|
||||
isMation,
|
||||
loading,
|
||||
notificationBadgeContent = 0,
|
||||
}: GenerateDrawerBottomLinksOptions) => {
|
||||
if (loading || isMation) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
Icon: NotificationsIcon,
|
||||
primary: 'settingsDrawer.notifications',
|
||||
to: URLS.UPDATES,
|
||||
badgeContent: notificationBadgeContent,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default function PublicLayout({
|
||||
children,
|
||||
}: PublicLayoutProps): React.ReactElement {
|
||||
const version = useVersion();
|
||||
const { isMation, loading } = useAutomatischInfo();
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||
@@ -60,6 +78,8 @@ export default function PublicLayout({
|
||||
|
||||
const drawerBottomLinks = generateDrawerBottomLinks({
|
||||
notificationBadgeContent: version.newVersionCount,
|
||||
loading,
|
||||
isMation,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import Typography from '@mui/material/Typography';
|
||||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import CustomLogo from 'components/CustomLogo/index.ee';
|
||||
import DefaultLogo from 'components/DefaultLogo';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
const Logo = () => {
|
||||
@@ -13,11 +12,7 @@ const Logo = () => {
|
||||
|
||||
if (logoSvgData) return <CustomLogo />;
|
||||
|
||||
return (
|
||||
<Typography variant="h6" component="h1" data-test="typography-logo" noWrap>
|
||||
<FormattedMessage id="brandText" />
|
||||
</Typography>
|
||||
);
|
||||
return <DefaultLogo />;
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
|
@@ -0,0 +1,3 @@
|
||||
<svg width="115" height="22" viewBox="0 0 411 77" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.991 15.3555C281.231 15.3555 284.669 11.918 284.669 7.67773C284.669 3.43744 281.231 0 276.991 0C272.751 0 269.313 3.43744 269.313 7.67773C269.313 11.918 272.751 15.3555 276.991 15.3555ZM367.497 50.0315C367.497 41.5346 374.385 34.6464 382.882 34.6464C391.379 34.6464 398.267 41.5346 398.267 50.0315V71.2076H409.851V50.0315C409.851 35.1371 397.777 23.0627 382.882 23.0627C367.988 23.0627 355.914 35.1371 355.914 50.0315V71.2076H367.497V50.0315ZM271.199 71.2071V28.8539H282.783V71.2071H271.199ZM237.933 34.6464V71.2076H249.517V34.6464H259.608V23.0627H249.517V7.67718H237.933V23.0627H227.843V34.6464H237.933ZM176.899 50.0296C176.899 58.5265 183.787 65.4146 192.284 65.4146V76.9983C177.389 76.9983 165.315 64.924 165.315 50.0296C165.315 35.1351 177.389 23.0608 192.284 23.0608C207.178 23.0608 219.252 35.1359 219.252 50.0303L219.253 71.2068H207.669L207.669 50.0303C207.669 41.5334 200.781 34.6445 192.284 34.6445C183.787 34.6445 176.899 41.5326 176.899 50.0296ZM71.0145 50.0315C71.0145 41.5346 77.9026 34.6464 86.3995 34.6464C94.8965 34.6464 101.785 41.5346 101.785 50.0315V71.2071H113.368V50.0315C113.368 41.5346 120.256 34.6464 128.753 34.6464C137.25 34.6464 144.138 41.5346 144.138 50.0315L144.138 71.2071H155.722L155.722 50.0315C155.722 35.1371 143.647 23.0627 128.753 23.0627C120.165 23.0627 112.515 27.0767 107.576 33.3308C102.637 27.0767 94.9873 23.0627 86.3995 23.0627C71.5051 23.0627 59.4308 35.1371 59.4308 50.0315V71.2071H71.0145V50.0315ZM44.0459 65.4162V76.9999H1.69178V65.4162H44.0459ZM292.376 50.0305C292.376 64.925 304.45 76.9993 319.345 76.9993C334.239 76.9993 346.313 64.9257 346.313 50.0313C346.313 35.1369 334.239 23.0618 319.345 23.0618C304.45 23.0618 292.376 35.1361 292.376 50.0305ZM319.345 65.4157C310.848 65.4157 303.96 58.5276 303.96 50.0306C303.96 41.5337 310.848 34.6456 319.345 34.6456C327.842 34.6456 334.729 41.5345 334.729 50.0314C334.729 58.5283 327.842 65.4157 319.345 65.4157Z" fill="#ffffff"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
8
packages/web/src/components/MationLogo/index.tsx
Normal file
8
packages/web/src/components/MationLogo/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { ReactComponent as MationLogoSvg } from './assets/mation-logo.svg';
|
||||
|
||||
const MationLogo = () => {
|
||||
return <MationLogoSvg />;
|
||||
};
|
||||
|
||||
export default MationLogo;
|
@@ -7,19 +7,20 @@ import * as React from 'react';
|
||||
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import theme from 'styles/theme';
|
||||
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||
import { defaultTheme, mationTheme } from 'styles/theme';
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const customizeTheme = (defaultTheme: typeof theme, config: IJSONObject) => {
|
||||
const customizeTheme = (theme: typeof defaultTheme, config: IJSONObject) => {
|
||||
// `clone` is needed so that the new theme reference triggers re-render
|
||||
const shallowDefaultTheme = clone(defaultTheme);
|
||||
const shallowDefaultTheme = clone(theme);
|
||||
|
||||
for (const key in config) {
|
||||
const value = config[key];
|
||||
const exists = get(defaultTheme, key);
|
||||
const exists = get(theme, key);
|
||||
|
||||
if (exists) {
|
||||
set(shallowDefaultTheme, key, value);
|
||||
@@ -33,18 +34,21 @@ const ThemeProvider = ({
|
||||
children,
|
||||
...props
|
||||
}: ThemeProviderProps): React.ReactElement => {
|
||||
const { config, loading } = useConfig();
|
||||
const { isMation, loading: automatischInfoLoading } = useAutomatischInfo();
|
||||
const { config, loading: configLoading } = useConfig();
|
||||
|
||||
const customTheme = React.useMemo(() => {
|
||||
if (!config) return theme;
|
||||
const installationTheme = isMation ? mationTheme : defaultTheme;
|
||||
|
||||
const customTheme = customizeTheme(theme, config);
|
||||
if (configLoading || automatischInfoLoading) return installationTheme;
|
||||
|
||||
const customTheme = customizeTheme(installationTheme, config || {});
|
||||
|
||||
return customTheme;
|
||||
}, [config]);
|
||||
}, [configLoading, config, isMation, automatischInfoLoading]);
|
||||
|
||||
// TODO: maybe a global loading state for the custom theme?
|
||||
if (loading) return <></>;
|
||||
if (automatischInfoLoading || configLoading) return <></>;
|
||||
|
||||
return (
|
||||
<BaseThemeProvider theme={customTheme} {...props}>
|
||||
|
@@ -103,13 +103,6 @@ export const ADMIN_APP_AUTH_CLIENTS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/au
|
||||
export const ADMIN_APP_CONNECTIONS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/connections`;
|
||||
export const ADMIN_APP_CONNECTIONS = (appKey: string) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/connections`;
|
||||
export const ADMIN_APP_CONNECTIONS_CREATE = (appKey: string, shared = false) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/connections/create?shared=${shared}`;
|
||||
export const ADMIN_APP_CONNECTIONS_CREATE_WITH_AUTH_CLIENT_ID = (
|
||||
appKey: string,
|
||||
appAuthClientId: string
|
||||
) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/connections/create?appAuthClientId=${appAuthClientId}`;
|
||||
export const ADMIN_APP_SETTINGS = (appKey: string) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/settings`;
|
||||
export const ADMIN_APP_AUTH_CLIENTS = (appKey: string) =>
|
||||
@@ -118,23 +111,6 @@ export const ADMIN_APP_AUTH_CLIENT = (appKey: string, id: string) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/${id}`;
|
||||
export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey: string) =>
|
||||
`${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/create`;
|
||||
export const ADMIN_APP_RECONNECT_CONNECTION = (
|
||||
appKey: string,
|
||||
connectionId: string,
|
||||
appAuthClientId?: string
|
||||
) => {
|
||||
const path = `${ADMIN_SETTINGS}/apps/${appKey}/connections/${connectionId}/reconnect`;
|
||||
|
||||
if (appAuthClientId) {
|
||||
return `${path}?appAuthClientId=${appAuthClientId}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
export const ADMIN_APP_SHARE_CONNECTION = (
|
||||
appKey: string,
|
||||
connectionId: string
|
||||
) => `${ADMIN_SETTINGS}/apps/${appKey}/connections/${connectionId}/share`;
|
||||
|
||||
export const DASHBOARD = FLOWS;
|
||||
|
||||
|
@@ -5,11 +5,15 @@ import { GET_AUTOMATISCH_INFO } from 'graphql/queries/get-automatisch-info';
|
||||
|
||||
export type AutomatischInfoContextParams = {
|
||||
isCloud: boolean;
|
||||
isMation: boolean;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export const AutomatischInfoContext =
|
||||
React.createContext<AutomatischInfoContextParams>({
|
||||
isCloud: false,
|
||||
isMation: false,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
type AutomatischInfoProviderProps = {
|
||||
@@ -23,13 +27,15 @@ export const AutomatischInfoProvider = (
|
||||
const { data, loading } = useQuery(GET_AUTOMATISCH_INFO);
|
||||
|
||||
const isCloud = data?.getAutomatischInfo?.isCloud;
|
||||
const isMation = data?.getAutomatischInfo?.isMation;
|
||||
|
||||
const value = React.useMemo(() => {
|
||||
return {
|
||||
isCloud,
|
||||
loading
|
||||
isMation,
|
||||
loading,
|
||||
};
|
||||
}, [isCloud, loading]);
|
||||
}, [isCloud, isMation, loading]);
|
||||
|
||||
return (
|
||||
<AutomatischInfoContext.Provider value={value}>
|
||||
|
@@ -1,9 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SHARE_CONNECTION = gql`
|
||||
mutation ShareConnection($input: ShareConnectionInput) {
|
||||
shareConnection(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
@@ -7,7 +7,6 @@ export const GET_APP_CONNECTIONS = gql`
|
||||
connections {
|
||||
id
|
||||
key
|
||||
shared
|
||||
reconnectable
|
||||
appAuthClientId
|
||||
verified
|
||||
|
@@ -4,6 +4,7 @@ export const GET_AUTOMATISCH_INFO = gql`
|
||||
query GetAutomatischInfo {
|
||||
getAutomatischInfo {
|
||||
isCloud
|
||||
isMation
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -1,7 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_SHARED_CONNECTION_ROLE_IDS = gql`
|
||||
query GetSharedConnectionRoleIds($id: String!) {
|
||||
getSharedConnectionRoleIds(id: $id)
|
||||
}
|
||||
`;
|
@@ -3,6 +3,8 @@ import { AutomatischInfoContext } from 'contexts/AutomatischInfo';
|
||||
|
||||
type UseAutomatischInfoReturn = {
|
||||
isCloud: boolean;
|
||||
isMation: boolean;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function useAutomatischInfo(): UseAutomatischInfoReturn {
|
||||
@@ -10,5 +12,7 @@ export default function useAutomatischInfo(): UseAutomatischInfoReturn {
|
||||
|
||||
return {
|
||||
isCloud: automatischInfoContext.isCloud,
|
||||
isMation: automatischInfoContext.isMation,
|
||||
loading: automatischInfoContext.loading,
|
||||
};
|
||||
}
|
||||
|
@@ -5,16 +5,13 @@ import { GET_ROLES } from 'graphql/queries/get-roles.ee';
|
||||
|
||||
type QueryResponse = {
|
||||
getRoles: IRole[];
|
||||
};
|
||||
}
|
||||
|
||||
export default function useRoles() {
|
||||
const { data, loading, error } = useQuery<QueryResponse>(GET_ROLES, {
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
const { data, loading } = useQuery<QueryResponse>(GET_ROLES, { context: { autoSnackbar: false } });
|
||||
|
||||
return {
|
||||
roles: data?.getRoles || [],
|
||||
loading,
|
||||
error,
|
||||
loading
|
||||
};
|
||||
}
|
||||
|
@@ -1,32 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { LazyQueryHookOptions, useLazyQuery } from '@apollo/client';
|
||||
|
||||
import { GET_SHARED_CONNECTION_ROLE_IDS } from 'graphql/queries/get-shared-connection-role-ids';
|
||||
|
||||
type QueryResponse = {
|
||||
getSharedConnectionRoleIds: string[];
|
||||
};
|
||||
|
||||
export default function useSharedConnectionRoleIds(
|
||||
connectionId: string,
|
||||
options?: LazyQueryHookOptions
|
||||
) {
|
||||
const [getSharedConnectionRoleIds, { data, loading, error }] =
|
||||
useLazyQuery<QueryResponse>(GET_SHARED_CONNECTION_ROLE_IDS, options);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connectionId) {
|
||||
getSharedConnectionRoleIds({
|
||||
variables: {
|
||||
id: connectionId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [connectionId]);
|
||||
|
||||
return {
|
||||
roleIds: data?.getSharedConnectionRoleIds || [],
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
@@ -265,21 +265,5 @@
|
||||
"authClient.buttonSubmit": "Submit",
|
||||
"authClient.inputName": "Name",
|
||||
"authClient.inputActive": "Active",
|
||||
"updateAuthClient.title": "Update auth client",
|
||||
"adminAppsConnections.noConnections": "You don't have any connections yet.",
|
||||
"adminAppsConnections.createConnection": "Create connection",
|
||||
"adminAppsConnections.deletedMessage": "The connection has been deleted.",
|
||||
"adminAppsConnections.addedAt": "added {datetime}",
|
||||
"adminAppsConnections.testing": "Testing...",
|
||||
"adminAppsConnections.testSuccessful": "Test successful",
|
||||
"adminAppsConnections.testFailed": "Test failed",
|
||||
"adminAppsConnections.testConnection": "Test connection",
|
||||
"adminAppsConnections.delete": "Delete",
|
||||
"adminAppsConnections.reconnect": "Reconnect",
|
||||
"adminAppsConnections.shareConnection": "Share connection",
|
||||
"adminAppsConnections.reconnectConnection": "Reconnect connection",
|
||||
"adminAppsConnections.callToDocs": "Visit <docsLink>our documentation</docsLink> to see how to add connection for {appName}.",
|
||||
"adminAppsConnections.submit": "Submit",
|
||||
"adminAppsConnections.selectAll": "Select all roles",
|
||||
"adminAppsConnections.deselectAll": "Deselect all roles"
|
||||
"updateAuthClient.title": "Update auth client"
|
||||
}
|
||||
|
@@ -27,26 +27,9 @@ import AdminApplicationSettings from 'components/AdminApplicationSettings';
|
||||
import AdminApplicationAuthClients from 'components/AdminApplicationAuthClients';
|
||||
import AdminApplicationCreateAuthClient from 'components/AdminApplicationCreateAuthClient';
|
||||
import AdminApplicationUpdateAuthClient from 'components/AdminApplicationUpdateAuthClient';
|
||||
import AdminApplicationConnections from 'components/AdminApplicationConnections';
|
||||
import AdminApplicationConnectionCreate from 'components/AdminApplicationConnectionCreate';
|
||||
import AdminApplicationConnectionShare from 'components/AdminApplicationConnectionShare';
|
||||
|
||||
type AdminApplicationParams = {
|
||||
appKey: string;
|
||||
connectionId?: string;
|
||||
};
|
||||
|
||||
const ReconnectConnection = (props: any): React.ReactElement => {
|
||||
const { application, onClose } = props;
|
||||
const { connectionId } = useParams() as AdminApplicationParams;
|
||||
|
||||
return (
|
||||
<AdminApplicationConnectionCreate
|
||||
onClose={onClose}
|
||||
application={application}
|
||||
connectionId={connectionId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default function AdminApplication(): React.ReactElement | null {
|
||||
@@ -74,7 +57,6 @@ export default function AdminApplication(): React.ReactElement | null {
|
||||
const app = data?.getApp || {};
|
||||
|
||||
const goToAuthClientsPage = () => navigate('auth-clients');
|
||||
const goToConnectionsPage = () => navigate('connections');
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
@@ -138,7 +120,7 @@ export default function AdminApplication(): React.ReactElement | null {
|
||||
/>
|
||||
<Route
|
||||
path={`/connections/*`}
|
||||
element={<AdminApplicationConnections appKey={appKey} />}
|
||||
element={<div>App connections</div>}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
@@ -171,33 +153,6 @@ export default function AdminApplication(): React.ReactElement | null {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/connections/create"
|
||||
element={
|
||||
<AdminApplicationConnectionCreate
|
||||
onClose={goToConnectionsPage}
|
||||
application={app}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/connections/:connectionId/reconnect"
|
||||
element={
|
||||
<ReconnectConnection
|
||||
application={app}
|
||||
onClose={goToConnectionsPage}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/connections/:connectionId/share"
|
||||
element={
|
||||
<AdminApplicationConnectionShare
|
||||
onClose={goToConnectionsPage}
|
||||
application={app}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
|
@@ -65,8 +65,8 @@ function RoleMappings({ provider, providerLoading }: RoleMappingsProps) {
|
||||
enqueueSnackbar(formatMessage('roleMappingsForm.successfullySaved'), {
|
||||
variant: 'success',
|
||||
SnackbarProps: {
|
||||
'data-test': 'snackbar-update-role-mappings-success',
|
||||
},
|
||||
'data-test': 'snackbar-update-role-mappings-success'
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -5,12 +5,13 @@ import Stack from '@mui/material/Stack';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Divider, Typography } from '@mui/material';
|
||||
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||
import TextField from 'components/TextField';
|
||||
import { Divider, Typography } from '@mui/material';
|
||||
|
||||
function generateRoleOptions(roles: IRole[]) {
|
||||
return roles?.map(({ name: label, id: value }) => ({ label, value }));
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
@@ -7,6 +8,8 @@ import Container from 'components/Container';
|
||||
import NotificationCard from 'components/NotificationCard';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useAutomatischInfo from 'hooks/useAutomatischInfo';
|
||||
import * as URLS from 'config/urls';
|
||||
|
||||
interface INotification {
|
||||
name: string;
|
||||
@@ -16,8 +19,19 @@ interface INotification {
|
||||
}
|
||||
|
||||
export default function Updates(): React.ReactElement {
|
||||
const navigate = useNavigate();
|
||||
const formatMessage = useFormatMessage();
|
||||
const { notifications } = useNotifications();
|
||||
const { isMation, loading } = useAutomatischInfo();
|
||||
|
||||
React.useEffect(
|
||||
function redirectToHomepageInMation() {
|
||||
if (!loading && isMation) {
|
||||
navigate(URLS.DASHBOARD);
|
||||
}
|
||||
},
|
||||
[loading, isMation]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 3 }}>
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { deepmerge } from '@mui/utils';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
import { createTheme, alpha } from '@mui/material/styles';
|
||||
import { cardActionAreaClasses } from '@mui/material/CardActionArea';
|
||||
|
||||
@@ -6,7 +8,7 @@ export const primaryMainColor = '#0059F7';
|
||||
export const primaryLightColor = '#4286FF';
|
||||
export const primaryDarkColor = '#001F52';
|
||||
|
||||
const extendedTheme = createTheme({
|
||||
export const defaultTheme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: primaryMainColor,
|
||||
@@ -280,4 +282,24 @@ const extendedTheme = createTheme({
|
||||
},
|
||||
});
|
||||
|
||||
export default extendedTheme;
|
||||
export const mationTheme = createTheme(deepmerge(defaultTheme, {
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#2962FF',
|
||||
light: '#448AFF',
|
||||
dark: '#2962FF',
|
||||
contrastText: '#fff',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }: { theme: Theme }) => ({
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default defaultTheme;
|
||||
|
35
yarn.lock
35
yarn.lock
@@ -7106,11 +7106,6 @@ commander@7.1.0:
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz"
|
||||
integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==
|
||||
|
||||
commander@^10.0.0:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
|
||||
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
|
||||
|
||||
commander@^2.20.0:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz"
|
||||
@@ -7126,7 +7121,7 @@ commander@^8.3.0:
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz"
|
||||
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
|
||||
|
||||
commander@^9.0.0:
|
||||
commander@^9.0.0, commander@^9.1.0:
|
||||
version "9.5.0"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz"
|
||||
integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
|
||||
@@ -12266,13 +12261,13 @@ klona@^2.0.4, klona@^2.0.5:
|
||||
resolved "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz"
|
||||
integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
|
||||
|
||||
knex@^2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/knex/-/knex-2.5.1.tgz#a6c6b449866cf4229f070c17411f23871ba52ef9"
|
||||
integrity sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==
|
||||
knex@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/knex/-/knex-2.4.0.tgz"
|
||||
integrity sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA==
|
||||
dependencies:
|
||||
colorette "2.0.19"
|
||||
commander "^10.0.0"
|
||||
commander "^9.1.0"
|
||||
debug "4.3.4"
|
||||
escalade "^3.1.1"
|
||||
esm "^3.2.25"
|
||||
@@ -12280,7 +12275,7 @@ knex@^2.5.1:
|
||||
getopts "2.3.0"
|
||||
interpret "^2.2.0"
|
||||
lodash "^4.17.21"
|
||||
pg-connection-string "2.6.1"
|
||||
pg-connection-string "2.5.0"
|
||||
rechoir "^0.8.0"
|
||||
resolve-from "^5.0.0"
|
||||
tarn "^3.0.2"
|
||||
@@ -13864,13 +13859,12 @@ object.values@^1.1.0, object.values@^1.1.5:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.19.1"
|
||||
|
||||
objection@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/objection/-/objection-3.1.1.tgz#b744d4ff13c01863d6edec773f1315c964442510"
|
||||
integrity sha512-v8dqQrFwZm9gRN3ZF4abF+hL6Jm5EbcUjOxVDan0lheOev0sggGGHBP8jgesZ68I0XXBjDFjGXCjTPZsWDu49A==
|
||||
objection@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/objection/-/objection-3.0.1.tgz"
|
||||
integrity sha512-rqNnyQE+C55UHjdpTOJEKQHJGZ/BGtBBtgxdUpKG4DQXRUmqxfmgS/MhPWxB9Pw0mLSVLEltr6soD4c0Sddy0Q==
|
||||
dependencies:
|
||||
ajv "^8.6.2"
|
||||
ajv-formats "^2.1.1"
|
||||
db-errors "^0.2.3"
|
||||
|
||||
obuf@^1.0.0, obuf@^1.1.2:
|
||||
@@ -14391,12 +14385,7 @@ performance-now@^2.1.0:
|
||||
resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"
|
||||
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
|
||||
|
||||
pg-connection-string@2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb"
|
||||
integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==
|
||||
|
||||
pg-connection-string@^2.5.0:
|
||||
pg-connection-string@2.5.0, pg-connection-string@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz"
|
||||
integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
|
||||
|
Reference in New Issue
Block a user