Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16d4ccd49d | ||
|
|
3c85cdfb87 | ||
|
|
1cd6cbb6dc | ||
|
|
852719d2c1 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,3 +1,8 @@
|
||||
node_modules/
|
||||
*/node_modules/
|
||||
*/build/
|
||||
|
||||
**/node_modules/
|
||||
**/build/
|
||||
.DS_Store
|
||||
.env
|
||||
49
FOLDER_STRUCTURE.md
Normal file
49
FOLDER_STRUCTURE.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Project Folder Structure
|
||||
|
||||
This document outlines the recommended GitHub-ready layout for your CulturalSync AI project.
|
||||
|
||||
```
|
||||
/
|
||||
├── client/ # Frontend (React + Tailwind)
|
||||
│ ├── public/
|
||||
│ └── src/
|
||||
│ ├── assets/
|
||||
│ ├── components/
|
||||
│ ├── hooks/
|
||||
│ ├── pages/
|
||||
│ ├── stores/
|
||||
│ ├── styles/
|
||||
│ └── utils/
|
||||
│ ├── package.json
|
||||
│ ├── tailwind.config.js
|
||||
│ └── tsconfig.json
|
||||
|
||||
├── server/ # Backend (Node.js + Express + MongoDB)
|
||||
│ ├── models/ # Mongoose schemas
|
||||
│ ├── routes/ # Express routers (users, orgs, workflows, etc.)
|
||||
│ ├── middleware/ # Auth, error handlers, RBAC checks
|
||||
│ ├── utils/ # Helpers (email, logger, config)
|
||||
│ ├── controllers/ # Business logic for each route
|
||||
│ ├── services/ # External integrations (n8n, X API, AI, etc.)
|
||||
│ ├── config/ # Database, environment variables, constants
|
||||
│ ├── index.js # App entry point
|
||||
│ ├── package.json
|
||||
│ └── .env.example
|
||||
|
||||
├── scripts/
|
||||
│ └── seed.js # Seed sample MongoDB data (orgs, users, workflows, logs)
|
||||
|
||||
├── Dockerfile # Multi-stage build for client & server
|
||||
├── docker-compose.yml # (Optional) local dev orchestration
|
||||
├── .gitignore
|
||||
├── README.md # Project overview & setup instructions
|
||||
└── FOLDER_STRUCTURE.md # (This file)
|
||||
```
|
||||
|
||||
Next steps:
|
||||
1. Move your existing frontend code into `client/src/`.
|
||||
2. Move backend code into `server/`, splitting models, routes, middleware, etc.
|
||||
3. Update root-level Dockerfile/docker-compose.yml to reference `client` and `server`.
|
||||
4. Drop `scripts/seed.js` in place for sample data.
|
||||
|
||||
Commit this structure before adding custom modules (n8n embed, AI prompts, compliance checks).
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Seeder: Enhance execution metrics and inject default workflow templates per organization
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// 1. Update all executions with random durationMs and alertCount
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE "Executions"
|
||||
SET "durationMs" = FLOOR(RANDOM() * (2000 - 200) + 200),
|
||||
"alertCount" = FLOOR(RANDOM() * 6)
|
||||
`);
|
||||
|
||||
// 2. Insert default workflow templates for each organization if not present
|
||||
const [orgs] = await queryInterface.sequelize.query(
|
||||
'SELECT id FROM "Organizations"'
|
||||
);
|
||||
|
||||
const templates = [
|
||||
{ name: "Lunar New Year Promo", description: "Localized campaign for Singapore market" },
|
||||
{ name: "Diwali WhatsApp Funnel", description: "WhatsApp push for India" },
|
||||
{ name: "Māori Festival Email", description: "NZ market outreach" },
|
||||
{ name: "GDPR Lead Magnet Funnel", description: "EU-compliant workflow" }
|
||||
];
|
||||
|
||||
for (const org of orgs) {
|
||||
for (const template of templates) {
|
||||
const [[existing]] = await queryInterface.sequelize.query(
|
||||
`SELECT id FROM "Workflows" WHERE name = :name AND "organizationId" = :orgId`,
|
||||
{
|
||||
replacements: { name: template.name, orgId: org.id },
|
||||
type: Sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
if (!existing) {
|
||||
await queryInterface.bulkInsert(
|
||||
'Workflows',
|
||||
[{
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
nodes: JSON.stringify([]),
|
||||
organizationId: org.id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}],
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
// Revert random metrics
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE "Executions"
|
||||
SET "durationMs" = NULL, "alertCount" = NULL
|
||||
`);
|
||||
|
||||
// Remove injected workflow templates
|
||||
await queryInterface.bulkDelete(
|
||||
'Workflows',
|
||||
{
|
||||
name: [
|
||||
"Lunar New Year Promo",
|
||||
"Diwali WhatsApp Funnel",
|
||||
"Māori Festival Email",
|
||||
"GDPR Lead Magnet Funnel"
|
||||
]
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -17,7 +17,14 @@ module.exports = class Helpers {
|
||||
return res.status(500).send(error.message);
|
||||
}
|
||||
|
||||
static jwtSign(data) {
|
||||
return jwt.sign(data, config.secret_key, { expiresIn: '6h' });
|
||||
static sanitizeWorkflow(workflow) {
|
||||
const { id, name, description, nodes, createdAt, updatedAt } = workflow;
|
||||
return { id, name, description, nodes, createdAt, updatedAt };
|
||||
}
|
||||
|
||||
static sanitizeOrganization(org) {
|
||||
const { id, name, region, industry, logoUrl, createdAt } = org;
|
||||
return { id, name, region, industry, logoUrl, createdAt };
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@ -128,7 +128,14 @@ app.use(
|
||||
|
||||
app.use(
|
||||
'/api/workflows',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
(req, res, next) => {
|
||||
const segments = req.path.split('/').filter(Boolean);
|
||||
const publicGetById = req.method === 'GET' && segments.length === 1;
|
||||
if (publicGetById) {
|
||||
return next();
|
||||
}
|
||||
return passport.authenticate('jwt', { session: false })(req, res, next);
|
||||
},
|
||||
workflowsRoutes,
|
||||
);
|
||||
|
||||
@ -146,7 +153,12 @@ app.use(
|
||||
|
||||
app.use(
|
||||
'/api/organizations',
|
||||
passport.authenticate('jwt', { session: false }),
|
||||
(req, res, next) => {
|
||||
const segments = req.path.split('/').filter(Boolean);
|
||||
const publicGetById = req.method === 'GET' && segments.length === 1;
|
||||
if (publicGetById) return next();
|
||||
return passport.authenticate('jwt', { session: false })(req, res, next);
|
||||
},
|
||||
organizationsRoutes,
|
||||
);
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ const express = require('express');
|
||||
const OrganizationsService = require('../services/organizations');
|
||||
const OrganizationsDBApi = require('../db/api/organizations');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const Helpers = require('../helpers');
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
|
||||
459
backend/src/routes/organizations.js.temp
Normal file
459
backend/src/routes/organizations.js.temp
Normal file
@ -0,0 +1,459 @@
|
||||
const express = require('express');
|
||||
|
||||
const OrganizationsService = require('../services/organizations');
|
||||
const OrganizationsDBApi = require('../db/api/organizations');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const Helpers = require('../helpers');
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const { parse } = require('json2csv');
|
||||
|
||||
const { checkCrudPermissions } = require('../middlewares/check-permissions');
|
||||
|
||||
router.use(checkCrudPermissions('organizations'));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Organizations:
|
||||
* type: object
|
||||
* properties:
|
||||
|
||||
* name:
|
||||
* type: string
|
||||
* default: name
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Organizations
|
||||
* description: The Organizations managing API
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/organizations:
|
||||
* post:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Organizations]
|
||||
* summary: Add new item
|
||||
* description: Add new item
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* data:
|
||||
* description: Data of the updated item
|
||||
* type: object
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item was successfully added
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 405:
|
||||
* description: Invalid input data
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.post(
|
||||
'/',
|
||||
wrapAsync(async (req, res) => {
|
||||
const referer =
|
||||
req.headers.referer ||
|
||||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||
const link = new URL(referer);
|
||||
await OrganizationsService.create(
|
||||
req.body.data,
|
||||
req.currentUser,
|
||||
true,
|
||||
link.host,
|
||||
);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/budgets/bulk-import:
|
||||
* post:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Organizations]
|
||||
* summary: Bulk import items
|
||||
* description: Bulk import items
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* data:
|
||||
* description: Data of the updated items
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The items were successfully imported
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 405:
|
||||
* description: Invalid input data
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*
|
||||
*/
|
||||
router.post(
|
||||
'/bulk-import',
|
||||
wrapAsync(async (req, res) => {
|
||||
const referer =
|
||||
req.headers.referer ||
|
||||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||
const link = new URL(referer);
|
||||
await OrganizationsService.bulkImport(req, res, true, link.host);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/organizations/{id}:
|
||||
* put:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Organizations]
|
||||
* summary: Update the data of the selected item
|
||||
* description: Update the data of the selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to update
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* requestBody:
|
||||
* description: Set new item data
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* id:
|
||||
* description: ID of the updated item
|
||||
* type: string
|
||||
* data:
|
||||
* description: Data of the updated item
|
||||
* type: object
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* required:
|
||||
* - id
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item data was successfully updated
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.put(
|
||||
'/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
await OrganizationsService.update(
|
||||
req.body.data,
|
||||
req.body.id,
|
||||
req.currentUser,
|
||||
);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/organizations/{id}:
|
||||
* delete:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Organizations]
|
||||
* summary: Delete the selected item
|
||||
* description: Delete the selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to delete
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item was successfully deleted
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.delete(
|
||||
'/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
await OrganizationsService.remove(req.params.id, req.currentUser);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/organizations/deleteByIds:
|
||||
* post:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Organizations]
|
||||
* summary: Delete the selected item list
|
||||
* description: Delete the selected item list
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* ids:
|
||||
* description: IDs of the updated items
|
||||
* type: array
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The items was successfully deleted
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Items not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.post(
|
||||
'/deleteByIds',
|
||||
wrapAsync(async (req, res) => {
|
||||
await OrganizationsService.deleteByIds(req.body.data, req.currentUser);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/organizations:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Organizations]
|
||||
* summary: Get all organizations
|
||||
* description: Get all organizations
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Organizations list successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get(
|
||||
'/',
|
||||
wrapAsync(async (req, res) => {
|
||||
const filetype = req.query.filetype;
|
||||
|
||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||
|
||||
const currentUser = req.currentUser;
|
||||
const payload = await OrganizationsDBApi.findAll(req.query, globalAccess, {
|
||||
currentUser,
|
||||
});
|
||||
if (filetype && filetype === 'csv') {
|
||||
const fields = ['id', 'name'];
|
||||
const opts = { fields };
|
||||
try {
|
||||
const csv = parse(payload.rows, opts);
|
||||
res.status(200).attachment(csv);
|
||||
res.send(csv);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
} else {
|
||||
res.status(200).send(payload);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/organizations/count:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Organizations]
|
||||
* summary: Count all organizations
|
||||
* description: Count all organizations
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Organizations count successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get(
|
||||
'/count',
|
||||
wrapAsync(async (req, res) => {
|
||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||
|
||||
const currentUser = req.currentUser;
|
||||
const payload = await OrganizationsDBApi.findAll(req.query, globalAccess, {
|
||||
countOnly: true,
|
||||
currentUser,
|
||||
});
|
||||
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/organizations/autocomplete:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Organizations]
|
||||
* summary: Find all organizations that match search criteria
|
||||
* description: Find all organizations that match search criteria
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Organizations list successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get('/autocomplete', async (req, res) => {
|
||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||
|
||||
const organizationId = req.currentUser.organization?.id;
|
||||
|
||||
const payload = await OrganizationsDBApi.findAllAutocomplete(
|
||||
req.query.query,
|
||||
req.query.limit,
|
||||
req.query.offset,
|
||||
globalAccess,
|
||||
organizationId,
|
||||
);
|
||||
|
||||
res.status(200).send(payload);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/organizations/{id}:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Organizations]
|
||||
* summary: Get selected item
|
||||
* description: Get selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: ID of item to get
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Selected item successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Organizations"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
router.get(
|
||||
'/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
const payload = await OrganizationsDBApi.findBy({ id: req.params.id });
|
||||
if (!payload) {
|
||||
return res.status(404).json({ error: 'Not found' });
|
||||
}
|
||||
res.json(Helpers.sanitizeOrganization(payload));
|
||||
})
|
||||
);
|
||||
);
|
||||
|
||||
router.use('/', require('../helpers').commonErrorHandler);
|
||||
|
||||
module.exports = router;
|
||||
@ -1,8 +1,11 @@
|
||||
const express = require('express');
|
||||
const db = require('../db/models');
|
||||
|
||||
|
||||
const WorkflowsService = require('../services/workflows');
|
||||
const WorkflowsDBApi = require('../db/api/workflows');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const Helpers = require('../helpers');
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
@ -439,6 +442,31 @@ router.get('/autocomplete', async (req, res) => {
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
// Route: Get latest execution status for a workflow
|
||||
router.get(
|
||||
'/:id/execution-status',
|
||||
wrapAsync(async (req, res) => {
|
||||
const workflowId = req.params.id;
|
||||
const execution = await db.executions.findOne({
|
||||
where: { workflowId },
|
||||
order: [['createdAt', 'DESC']],
|
||||
include: [{ model: db.compliance_logs, as: 'compliance_logs_execution' }],
|
||||
});
|
||||
if (!execution) {
|
||||
return res.status(404).json({ error: 'No execution found for workflow.' });
|
||||
}
|
||||
const alerts = execution.compliance_logs_execution
|
||||
? execution.compliance_logs_execution.map((log) => log.message)
|
||||
: [];
|
||||
res.json({
|
||||
status: execution.status,
|
||||
durationMs: execution.durationMs,
|
||||
outputSnippet: execution.output_snippet,
|
||||
alerts,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get(
|
||||
|
||||
486
backend/src/routes/workflows.js.temp
Normal file
486
backend/src/routes/workflows.js.temp
Normal file
@ -0,0 +1,486 @@
|
||||
const express = require('express');
|
||||
const db = require('../db/models');
|
||||
|
||||
|
||||
const WorkflowsService = require('../services/workflows');
|
||||
const WorkflowsDBApi = require('../db/api/workflows');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const Helpers = require('../helpers');
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const { parse } = require('json2csv');
|
||||
|
||||
const { checkCrudPermissions } = require('../middlewares/check-permissions');
|
||||
|
||||
router.use(checkCrudPermissions('workflows'));
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Workflows:
|
||||
* type: object
|
||||
* properties:
|
||||
|
||||
* name:
|
||||
* type: string
|
||||
* default: name
|
||||
* description:
|
||||
* type: string
|
||||
* default: description
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Workflows
|
||||
* description: The Workflows managing API
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/workflows:
|
||||
* post:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Workflows]
|
||||
* summary: Add new item
|
||||
* description: Add new item
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* data:
|
||||
* description: Data of the updated item
|
||||
* type: object
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item was successfully added
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 405:
|
||||
* description: Invalid input data
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.post(
|
||||
'/',
|
||||
wrapAsync(async (req, res) => {
|
||||
const referer =
|
||||
req.headers.referer ||
|
||||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||
const link = new URL(referer);
|
||||
await WorkflowsService.create(
|
||||
req.body.data,
|
||||
req.currentUser,
|
||||
true,
|
||||
link.host,
|
||||
);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/budgets/bulk-import:
|
||||
* post:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Workflows]
|
||||
* summary: Bulk import items
|
||||
* description: Bulk import items
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* data:
|
||||
* description: Data of the updated items
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The items were successfully imported
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 405:
|
||||
* description: Invalid input data
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*
|
||||
*/
|
||||
router.post(
|
||||
'/bulk-import',
|
||||
wrapAsync(async (req, res) => {
|
||||
const referer =
|
||||
req.headers.referer ||
|
||||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||
const link = new URL(referer);
|
||||
await WorkflowsService.bulkImport(req, res, true, link.host);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/workflows/{id}:
|
||||
* put:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Workflows]
|
||||
* summary: Update the data of the selected item
|
||||
* description: Update the data of the selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to update
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* requestBody:
|
||||
* description: Set new item data
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* id:
|
||||
* description: ID of the updated item
|
||||
* type: string
|
||||
* data:
|
||||
* description: Data of the updated item
|
||||
* type: object
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* required:
|
||||
* - id
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item data was successfully updated
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.put(
|
||||
'/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
await WorkflowsService.update(req.body.data, req.body.id, req.currentUser);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/workflows/{id}:
|
||||
* delete:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Workflows]
|
||||
* summary: Delete the selected item
|
||||
* description: Delete the selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: Item ID to delete
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The item was successfully deleted
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.delete(
|
||||
'/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
await WorkflowsService.remove(req.params.id, req.currentUser);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/workflows/deleteByIds:
|
||||
* post:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Workflows]
|
||||
* summary: Delete the selected item list
|
||||
* description: Delete the selected item list
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* ids:
|
||||
* description: IDs of the updated items
|
||||
* type: array
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The items was successfully deleted
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Items not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.post(
|
||||
'/deleteByIds',
|
||||
wrapAsync(async (req, res) => {
|
||||
await WorkflowsService.deleteByIds(req.body.data, req.currentUser);
|
||||
const payload = true;
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/workflows:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Workflows]
|
||||
* summary: Get all workflows
|
||||
* description: Get all workflows
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Workflows list successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get(
|
||||
'/',
|
||||
wrapAsync(async (req, res) => {
|
||||
const filetype = req.query.filetype;
|
||||
|
||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||
|
||||
const currentUser = req.currentUser;
|
||||
const payload = await WorkflowsDBApi.findAll(req.query, globalAccess, {
|
||||
currentUser,
|
||||
});
|
||||
if (filetype && filetype === 'csv') {
|
||||
const fields = ['id', 'name', 'description'];
|
||||
const opts = { fields };
|
||||
try {
|
||||
const csv = parse(payload.rows, opts);
|
||||
res.status(200).attachment(csv);
|
||||
res.send(csv);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
} else {
|
||||
res.status(200).send(payload);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/workflows/count:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Workflows]
|
||||
* summary: Count all workflows
|
||||
* description: Count all workflows
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Workflows count successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get(
|
||||
'/count',
|
||||
wrapAsync(async (req, res) => {
|
||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||
|
||||
const currentUser = req.currentUser;
|
||||
const payload = await WorkflowsDBApi.findAll(req.query, globalAccess, {
|
||||
countOnly: true,
|
||||
currentUser,
|
||||
});
|
||||
|
||||
res.status(200).send(payload);
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/workflows/autocomplete:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Workflows]
|
||||
* summary: Find all workflows that match search criteria
|
||||
* description: Find all workflows that match search criteria
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Workflows list successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Data not found
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get('/autocomplete', async (req, res) => {
|
||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||
|
||||
const organizationId = req.currentUser.organization?.id;
|
||||
|
||||
const payload = await WorkflowsDBApi.findAllAutocomplete(
|
||||
req.query.query,
|
||||
req.query.limit,
|
||||
req.query.offset,
|
||||
globalAccess,
|
||||
organizationId,
|
||||
);
|
||||
|
||||
res.status(200).send(payload);
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/workflows/{id}:
|
||||
* get:
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* tags: [Workflows]
|
||||
* summary: Get selected item
|
||||
* description: Get selected item
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* description: ID of item to get
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Selected item successfully received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: "#/components/schemas/Workflows"
|
||||
* 400:
|
||||
* description: Invalid ID supplied
|
||||
* 401:
|
||||
* $ref: "#/components/responses/UnauthorizedError"
|
||||
* 404:
|
||||
* description: Item not found
|
||||
* 500:
|
||||
// Route: Get latest execution status for a workflow
|
||||
router.get(
|
||||
'/:id/execution-status',
|
||||
wrapAsync(async (req, res) => {
|
||||
const workflowId = req.params.id;
|
||||
const execution = await db.executions.findOne({
|
||||
where: { workflowId },
|
||||
order: [['createdAt', 'DESC']],
|
||||
include: [{ model: db.compliance_logs, as: 'compliance_logs_execution' }],
|
||||
});
|
||||
if (!execution) {
|
||||
return res.status(404).json({ error: 'No execution found for workflow.' });
|
||||
}
|
||||
const alerts = execution.compliance_logs_execution
|
||||
? execution.compliance_logs_execution.map((log) => log.message)
|
||||
: [];
|
||||
res.json({
|
||||
status: execution.status,
|
||||
durationMs: execution.durationMs,
|
||||
outputSnippet: execution.output_snippet,
|
||||
alerts,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
* description: Some server error
|
||||
*/
|
||||
router.get(
|
||||
'/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
const payload = await WorkflowsDBApi.findBy({ id: req.params.id });
|
||||
if (!payload) {
|
||||
return res.status(404).json({ error: 'Not found' });
|
||||
}
|
||||
res.json(Helpers.sanitizeWorkflow(payload));
|
||||
})
|
||||
);
|
||||
);
|
||||
|
||||
router.use('/', require('../helpers').commonErrorHandler);
|
||||
|
||||
module.exports = router;
|
||||
1
frontend/json/runtimeError.json
Normal file
1
frontend/json/runtimeError.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
@ -60,6 +60,15 @@ const menuAside: MenuAsideItem[] = [
|
||||
: icon.mdiTable ?? icon.mdiTable,
|
||||
permissions: 'READ_WORKFLOWS',
|
||||
},
|
||||
{
|
||||
href: '/workflows/builder',
|
||||
label: 'Builder',
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
icon: icon['mdiWand2'] ?? icon.mdiTable,
|
||||
permissions: 'READ_WORKFLOWS',
|
||||
},
|
||||
|
||||
{
|
||||
href: '/roles/roles-list',
|
||||
label: 'Roles',
|
||||
|
||||
108
frontend/src/pages/workflows/builder.tsx
Normal file
108
frontend/src/pages/workflows/builder.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useRouter } from 'next/router';
|
||||
import Head from 'next/head';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
|
||||
const WorkflowsBuilderPage = () => {
|
||||
const router = useRouter();
|
||||
const { workflowId: paramId } = router.query;
|
||||
const [workflows, setWorkflows] = useState([]);
|
||||
const [workflowId, setWorkflowId] = useState(paramId || '');
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [executionStatus, setExecutionStatus] = useState(null);
|
||||
const [polling, setPolling] = useState(false);
|
||||
const pollRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
axios.get('/workflows')
|
||||
.then(res => setWorkflows(res.data.rows || []))
|
||||
.catch(err => console.error('Error fetching workflows:', err));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (paramId) {
|
||||
setWorkflowId(paramId.toString());
|
||||
}
|
||||
}, [paramId]);
|
||||
|
||||
const executeWorkflow = async () => {
|
||||
if (!workflowId) return;
|
||||
setExecuting(true);
|
||||
setMessage('');
|
||||
try {
|
||||
const res = await axios.post(`/workflows/${workflowId}/execute`);
|
||||
setMessage(res.data.success ? 'Execution started.' : 'Execution failed.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setMessage('Error triggering execution');
|
||||
}
|
||||
setExecuting(false);
|
||||
};
|
||||
|
||||
const srcUrl = workflowId
|
||||
? `http://localhost:5678/workflow/${workflowId}`
|
||||
: 'http://localhost:5678/workflow';
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full bg-white p-4 flex flex-col">
|
||||
<Head>
|
||||
<title>Workflow Builder | CulturalSync AI</title>
|
||||
</Head>
|
||||
<h1 className="text-2xl font-semibold mb-4">Workflow Builder</h1>
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<select
|
||||
className="p-2 border rounded"
|
||||
value={workflowId}
|
||||
onChange={e => setWorkflowId(e.target.value)}
|
||||
>
|
||||
<option value="">Select a workflow...</option>
|
||||
{workflows.map(wf => (
|
||||
<option key={wf.id} value={wf.id}>{wf.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={executeWorkflow}
|
||||
disabled={!workflowId || executing}
|
||||
className={`px-4 py-2 rounded text-white ${executing ? 'bg-gray-400' : 'bg-indigo-600 hover:bg-indigo-700'}`}
|
||||
>
|
||||
{executing ? 'Executing...' : 'Run Workflow'}
|
||||
|
||||
{executionStatus && (
|
||||
<div className="mt-6 p-4 border rounded-lg bg-gray-50">
|
||||
<p><strong>Status:</strong> {executionStatus.status}</p>
|
||||
<p><strong>Duration:</strong> {executionStatus.durationMs} ms</p>
|
||||
<p><strong>Output:</strong> {executionStatus.outputSnippet?.slice(0, 300)}...</p>
|
||||
{executionStatus.alerts?.length > 0 && (
|
||||
<div className="text-red-600 mt-2">
|
||||
<strong>Compliance Alerts:</strong>
|
||||
<ul>
|
||||
{executionStatus.alerts.map((alert, i) => <li key={i}>⚠️ {alert}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</button>
|
||||
</div>
|
||||
{message && <div className="mb-2 text-sm text-green-600">{message}</div>}
|
||||
<iframe
|
||||
src={srcUrl}
|
||||
title="n8n Embedded Canvas"
|
||||
width="100%"
|
||||
height="calc(100vh - 160px)"
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
className="rounded-xl shadow-md border flex-1"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WorkflowsBuilderPage.getLayout = function getLayout(page) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default WorkflowsBuilderPage;
|
||||
112
frontend/src/pages/workflows/builder.tsx.temp
Normal file
112
frontend/src/pages/workflows/builder.tsx.temp
Normal file
@ -0,0 +1,112 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useRouter } from 'next/router';
|
||||
import Head from 'next/head';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
|
||||
const WorkflowsBuilderPage = () => {
|
||||
const router = useRouter();
|
||||
const { workflowId: paramId } = router.query;
|
||||
const [workflows, setWorkflows] = useState([]);
|
||||
const [workflowId, setWorkflowId] = useState(paramId || '');
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [executionStatus, setExecutionStatus] = useState(null);
|
||||
const [polling, setPolling] = useState(false);
|
||||
const pollRef = useRef(null);
|
||||
|
||||
const [executionStatus, setExecutionStatus] = useState(null);
|
||||
const [polling, setPolling] = useState(false);
|
||||
const pollRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
axios.get('/workflows')
|
||||
.then(res => setWorkflows(res.data.rows || []))
|
||||
.catch(err => console.error('Error fetching workflows:', err));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (paramId) {
|
||||
setWorkflowId(paramId.toString());
|
||||
}
|
||||
}, [paramId]);
|
||||
|
||||
const executeWorkflow = async () => {
|
||||
if (!workflowId) return;
|
||||
setExecuting(true);
|
||||
setMessage('');
|
||||
try {
|
||||
const res = await axios.post(`/workflows/${workflowId}/execute`);
|
||||
setMessage(res.data.success ? 'Execution started.' : 'Execution failed.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setMessage('Error triggering execution');
|
||||
}
|
||||
setExecuting(false);
|
||||
};
|
||||
|
||||
const srcUrl = workflowId
|
||||
? `http://localhost:5678/workflow/${workflowId}`
|
||||
: 'http://localhost:5678/workflow';
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full bg-white p-4 flex flex-col">
|
||||
<Head>
|
||||
<title>Workflow Builder | CulturalSync AI</title>
|
||||
</Head>
|
||||
<h1 className="text-2xl font-semibold mb-4">Workflow Builder</h1>
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<select
|
||||
className="p-2 border rounded"
|
||||
value={workflowId}
|
||||
onChange={e => setWorkflowId(e.target.value)}
|
||||
>
|
||||
<option value="">Select a workflow...</option>
|
||||
{workflows.map(wf => (
|
||||
<option key={wf.id} value={wf.id}>{wf.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={executeWorkflow}
|
||||
disabled={!workflowId || executing}
|
||||
className={`px-4 py-2 rounded text-white ${executing ? 'bg-gray-400' : 'bg-indigo-600 hover:bg-indigo-700'}`}
|
||||
>
|
||||
{executing ? 'Executing...' : 'Run Workflow'}
|
||||
|
||||
{executionStatus && (
|
||||
<div className="mt-6 p-4 border rounded-lg bg-gray-50">
|
||||
<p><strong>Status:</strong> {executionStatus.status}</p>
|
||||
<p><strong>Duration:</strong> {executionStatus.durationMs} ms</p>
|
||||
<p><strong>Output:</strong> {executionStatus.outputSnippet?.slice(0, 300)}...</p>
|
||||
{executionStatus.alerts?.length > 0 && (
|
||||
<div className="text-red-600 mt-2">
|
||||
<strong>Compliance Alerts:</strong>
|
||||
<ul>
|
||||
{executionStatus.alerts.map((alert, i) => <li key={i}>⚠️ {alert}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</button>
|
||||
</div>
|
||||
{message && <div className="mb-2 text-sm text-green-600">{message}</div>}
|
||||
<iframe
|
||||
src={srcUrl}
|
||||
title="n8n Embedded Canvas"
|
||||
width="100%"
|
||||
height="calc(100vh - 160px)"
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
className="rounded-xl shadow-md border flex-1"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WorkflowsBuilderPage.getLayout = function getLayout(page) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default WorkflowsBuilderPage;
|
||||
Loading…
x
Reference in New Issue
Block a user