From a95122e12095ae2ad7440aa50090ae5249a5c785 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 30 Apr 2025 20:50:06 +0000 Subject: [PATCH] add websites --- .gitignore | 5 + app-shell/src/_schema.json | 7 +- backend/src/db/api/ai_agents.js | 42 ++ backend/src/db/api/organizations.js | 5 + backend/src/db/api/websites.js | 331 ++++++++++++ backend/src/db/migrations/1746045849977.js | 90 ++++ backend/src/db/migrations/1746045881783.js | 47 ++ backend/src/db/migrations/1746045903232.js | 47 ++ backend/src/db/migrations/1746045929263.js | 49 ++ backend/src/db/migrations/1746045953413.js | 54 ++ backend/src/db/models/ai_agents.js | 8 + backend/src/db/models/organizations.js | 8 + backend/src/db/models/websites.js | 73 +++ .../db/seeders/20200430130760-user-roles.js | 51 ++ .../db/seeders/20231127130745-sample-data.js | 241 ++++++++- backend/src/db/seeders/20250430204409.js | 87 ++++ backend/src/index.js | 8 + backend/src/routes/websites.js | 458 +++++++++++++++++ backend/src/services/search.js | 2 + backend/src/services/websites.js | 114 +++++ frontend/json/runtimeError.json | 1 + .../components/Ai_agents/CardAi_agents.tsx | 11 + .../components/Ai_agents/ListAi_agents.tsx | 7 + .../Ai_agents/configureAi_agentsCols.tsx | 20 + .../components/WebPageComponents/Footer.tsx | 2 +- .../components/WebPageComponents/Header.tsx | 2 +- .../src/components/Websites/CardWebsites.tsx | 123 +++++ .../src/components/Websites/ListWebsites.tsx | 100 ++++ .../src/components/Websites/TableWebsites.tsx | 481 ++++++++++++++++++ .../Websites/configureWebsitesCols.tsx | 97 ++++ .../WidgetCreator/WidgetCreator.tsx | 2 +- frontend/src/helpers/dataFormatter.js | 19 + frontend/src/menuAside.ts | 8 + .../src/pages/ai_agents/[ai_agentsId].tsx | 13 + .../src/pages/ai_agents/ai_agents-edit.tsx | 13 + .../src/pages/ai_agents/ai_agents-list.tsx | 2 + .../src/pages/ai_agents/ai_agents-new.tsx | 12 + .../src/pages/ai_agents/ai_agents-table.tsx | 2 + .../src/pages/ai_agents/ai_agents-view.tsx | 6 + frontend/src/pages/dashboard.tsx | 35 ++ .../organizations/organizations-view.tsx | 45 ++ frontend/src/pages/web_pages/about.tsx | 2 +- frontend/src/pages/websites/[websitesId].tsx | 153 ++++++ frontend/src/pages/websites/websites-edit.tsx | 151 ++++++ frontend/src/pages/websites/websites-list.tsx | 166 ++++++ frontend/src/pages/websites/websites-new.tsx | 122 +++++ .../src/pages/websites/websites-table.tsx | 165 ++++++ frontend/src/pages/websites/websites-view.tsx | 144 ++++++ frontend/src/stores/store.ts | 2 + frontend/src/stores/websites/websitesSlice.ts | 236 +++++++++ 50 files changed, 3858 insertions(+), 11 deletions(-) create mode 100644 backend/src/db/api/websites.js create mode 100644 backend/src/db/migrations/1746045849977.js create mode 100644 backend/src/db/migrations/1746045881783.js create mode 100644 backend/src/db/migrations/1746045903232.js create mode 100644 backend/src/db/migrations/1746045929263.js create mode 100644 backend/src/db/migrations/1746045953413.js create mode 100644 backend/src/db/models/websites.js create mode 100644 backend/src/db/seeders/20250430204409.js create mode 100644 backend/src/routes/websites.js create mode 100644 backend/src/services/websites.js create mode 100644 frontend/json/runtimeError.json create mode 100644 frontend/src/components/Websites/CardWebsites.tsx create mode 100644 frontend/src/components/Websites/ListWebsites.tsx create mode 100644 frontend/src/components/Websites/TableWebsites.tsx create mode 100644 frontend/src/components/Websites/configureWebsitesCols.tsx create mode 100644 frontend/src/pages/websites/[websitesId].tsx create mode 100644 frontend/src/pages/websites/websites-edit.tsx create mode 100644 frontend/src/pages/websites/websites-list.tsx create mode 100644 frontend/src/pages/websites/websites-new.tsx create mode 100644 frontend/src/pages/websites/websites-table.tsx create mode 100644 frontend/src/pages/websites/websites-view.tsx create mode 100644 frontend/src/stores/websites/websitesSlice.ts diff --git a/.gitignore b/.gitignore index e427ff3..d0eb167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules/ */node_modules/ */build/ + +**/node_modules/ +**/build/ +.DS_Store +.env \ No newline at end of file diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index a987528..2af1d95 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"oCaOYiQy/cfpN+f+\",\"encryptedData\":\"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\"}" -} + "Initial version": "{\"iv\":\"oCaOYiQy/cfpN+f+\",\"encryptedData\":\"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\"}", + "add websites": "{\"iv\":\"RfrHIvfGxItdXg3N\",\"encryptedData\":\"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\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/ai_agents.js b/backend/src/db/api/ai_agents.js index d5606e1..585ad31 100644 --- a/backend/src/db/api/ai_agents.js +++ b/backend/src/db/api/ai_agents.js @@ -29,6 +29,10 @@ module.exports = class Ai_agentsDBApi { transaction, }); + await ai_agents.setWebsite(data.website || null, { + transaction, + }); + return ai_agents; } @@ -88,6 +92,14 @@ module.exports = class Ai_agentsDBApi { ); } + if (data.website !== undefined) { + await ai_agents.setWebsite( + data.website, + + { transaction }, + ); + } + return ai_agents; } @@ -157,6 +169,10 @@ module.exports = class Ai_agentsDBApi { transaction, }); + output.website = await ai_agents.getWebsite({ + transaction, + }); + return output; } @@ -186,6 +202,32 @@ module.exports = class Ai_agentsDBApi { model: db.organizations, as: 'organizations', }, + + { + model: db.websites, + as: 'website', + + where: filter.website + ? { + [Op.or]: [ + { + id: { + [Op.in]: filter.website + .split('|') + .map((term) => Utils.uuid(term)), + }, + }, + { + name: { + [Op.or]: filter.website + .split('|') + .map((term) => ({ [Op.iLike]: `%${term}%` })), + }, + }, + ], + } + : {}, + }, ]; if (filter) { diff --git a/backend/src/db/api/organizations.js b/backend/src/db/api/organizations.js index 1d5703a..a2435cc 100644 --- a/backend/src/db/api/organizations.js +++ b/backend/src/db/api/organizations.js @@ -148,6 +148,11 @@ module.exports = class OrganizationsDBApi { transaction, }); + output.websites_organizations = + await organizations.getWebsites_organizations({ + transaction, + }); + return output; } diff --git a/backend/src/db/api/websites.js b/backend/src/db/api/websites.js new file mode 100644 index 0000000..9fe3b31 --- /dev/null +++ b/backend/src/db/api/websites.js @@ -0,0 +1,331 @@ +const db = require('../models'); +const FileDBApi = require('./file'); +const crypto = require('crypto'); +const Utils = require('../utils'); + +const Sequelize = db.Sequelize; +const Op = Sequelize.Op; + +module.exports = class WebsitesDBApi { + static async create(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const websites = await db.websites.create( + { + id: data.id || undefined, + + name: data.name || null, + url: data.url || null, + description: data.description || null, + importHash: data.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + + await websites.setOrganizations(data.organizations || null, { + transaction, + }); + + return websites; + } + + static async bulkImport(data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + // Prepare data - wrapping individual data transformations in a map() method + const websitesData = data.map((item, index) => ({ + id: item.id || undefined, + + name: item.name || null, + url: item.url || null, + description: item.description || null, + importHash: item.importHash || null, + createdById: currentUser.id, + updatedById: currentUser.id, + createdAt: new Date(Date.now() + index * 1000), + })); + + // Bulk create items + const websites = await db.websites.bulkCreate(websitesData, { + transaction, + }); + + // For each item created, replace relation files + + return websites; + } + + static async update(id, data, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + const globalAccess = currentUser.app_role?.globalAccess; + + const websites = await db.websites.findByPk(id, {}, { transaction }); + + const updatePayload = {}; + + if (data.name !== undefined) updatePayload.name = data.name; + + if (data.url !== undefined) updatePayload.url = data.url; + + if (data.description !== undefined) + updatePayload.description = data.description; + + updatePayload.updatedById = currentUser.id; + + await websites.update(updatePayload, { transaction }); + + if (data.organizations !== undefined) { + await websites.setOrganizations( + data.organizations, + + { transaction }, + ); + } + + return websites; + } + + static async deleteByIds(ids, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const websites = await db.websites.findAll({ + where: { + id: { + [Op.in]: ids, + }, + }, + transaction, + }); + + await db.sequelize.transaction(async (transaction) => { + for (const record of websites) { + await record.update({ deletedBy: currentUser.id }, { transaction }); + } + for (const record of websites) { + await record.destroy({ transaction }); + } + }); + + return websites; + } + + static async remove(id, options) { + const currentUser = (options && options.currentUser) || { id: null }; + const transaction = (options && options.transaction) || undefined; + + const websites = await db.websites.findByPk(id, options); + + await websites.update( + { + deletedBy: currentUser.id, + }, + { + transaction, + }, + ); + + await websites.destroy({ + transaction, + }); + + return websites; + } + + static async findBy(where, options) { + const transaction = (options && options.transaction) || undefined; + + const websites = await db.websites.findOne({ where }, { transaction }); + + if (!websites) { + return websites; + } + + const output = websites.get({ plain: true }); + + output.ai_agents_website = await websites.getAi_agents_website({ + transaction, + }); + + output.organizations = await websites.getOrganizations({ + transaction, + }); + + return output; + } + + static async findAll(filter, globalAccess, options) { + const limit = filter.limit || 0; + let offset = 0; + let where = {}; + const currentPage = +filter.page; + + const user = (options && options.currentUser) || null; + const userOrganizations = (user && user.organizations?.id) || null; + + if (userOrganizations) { + if (options?.currentUser?.organizationsId) { + where.organizationsId = options.currentUser.organizationsId; + } + } + + offset = currentPage * limit; + + const orderBy = null; + + const transaction = (options && options.transaction) || undefined; + + let include = [ + { + model: db.organizations, + as: 'organizations', + }, + ]; + + if (filter) { + if (filter.id) { + where = { + ...where, + ['id']: Utils.uuid(filter.id), + }; + } + + if (filter.name) { + where = { + ...where, + [Op.and]: Utils.ilike('websites', 'name', filter.name), + }; + } + + if (filter.url) { + where = { + ...where, + [Op.and]: Utils.ilike('websites', 'url', filter.url), + }; + } + + if (filter.description) { + where = { + ...where, + [Op.and]: Utils.ilike('websites', 'description', filter.description), + }; + } + + if (filter.active !== undefined) { + where = { + ...where, + active: filter.active === true || filter.active === 'true', + }; + } + + if (filter.organizations) { + const listItems = filter.organizations.split('|').map((item) => { + return Utils.uuid(item); + }); + + where = { + ...where, + organizationsId: { [Op.or]: listItems }, + }; + } + + if (filter.createdAtRange) { + const [start, end] = filter.createdAtRange; + + if (start !== undefined && start !== null && start !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.gte]: start, + }, + }; + } + + if (end !== undefined && end !== null && end !== '') { + where = { + ...where, + ['createdAt']: { + ...where.createdAt, + [Op.lte]: end, + }, + }; + } + } + } + + if (globalAccess) { + delete where.organizationsId; + } + + const queryOptions = { + where, + include, + distinct: true, + order: + filter.field && filter.sort + ? [[filter.field, filter.sort]] + : [['createdAt', 'desc']], + transaction: options?.transaction, + logging: console.log, + }; + + if (!options?.countOnly) { + queryOptions.limit = limit ? Number(limit) : undefined; + queryOptions.offset = offset ? Number(offset) : undefined; + } + + try { + const { rows, count } = await db.websites.findAndCountAll(queryOptions); + + return { + rows: options?.countOnly ? [] : rows, + count: count, + }; + } catch (error) { + console.error('Error executing query:', error); + throw error; + } + } + + static async findAllAutocomplete( + query, + limit, + offset, + globalAccess, + organizationId, + ) { + let where = {}; + + if (!globalAccess && organizationId) { + where.organizationId = organizationId; + } + + if (query) { + where = { + [Op.or]: [ + { ['id']: Utils.uuid(query) }, + Utils.ilike('websites', 'name', query), + ], + }; + } + + const records = await db.websites.findAll({ + attributes: ['id', 'name'], + where, + limit: limit ? Number(limit) : undefined, + offset: offset ? Number(offset) : undefined, + orderBy: [['name', 'ASC']], + }); + + return records.map((record) => ({ + id: record.id, + label: record.name, + })); + } +}; diff --git a/backend/src/db/migrations/1746045849977.js b/backend/src/db/migrations/1746045849977.js new file mode 100644 index 0000000..e3d5586 --- /dev/null +++ b/backend/src/db/migrations/1746045849977.js @@ -0,0 +1,90 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'websites', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.addColumn( + 'websites', + 'organizationsId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'organizations', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('websites', 'organizationsId', { + transaction, + }); + + await queryInterface.dropTable('websites', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1746045881783.js b/backend/src/db/migrations/1746045881783.js new file mode 100644 index 0000000..095c52f --- /dev/null +++ b/backend/src/db/migrations/1746045881783.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'websites', + 'name', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('websites', 'name', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1746045903232.js b/backend/src/db/migrations/1746045903232.js new file mode 100644 index 0000000..bd9e82b --- /dev/null +++ b/backend/src/db/migrations/1746045903232.js @@ -0,0 +1,47 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'websites', + 'url', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('websites', 'url', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1746045929263.js b/backend/src/db/migrations/1746045929263.js new file mode 100644 index 0000000..35d9329 --- /dev/null +++ b/backend/src/db/migrations/1746045929263.js @@ -0,0 +1,49 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'websites', + 'description', + { + type: Sequelize.DataTypes.TEXT, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('websites', 'description', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/migrations/1746045953413.js b/backend/src/db/migrations/1746045953413.js new file mode 100644 index 0000000..41286c3 --- /dev/null +++ b/backend/src/db/migrations/1746045953413.js @@ -0,0 +1,54 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.addColumn( + 'ai_agents', + 'websiteId', + { + type: Sequelize.DataTypes.UUID, + + references: { + model: 'websites', + key: 'id', + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.removeColumn('ai_agents', 'websiteId', { + transaction, + }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/ai_agents.js b/backend/src/db/models/ai_agents.js index c1c7f47..3cce76f 100644 --- a/backend/src/db/models/ai_agents.js +++ b/backend/src/db/models/ai_agents.js @@ -60,6 +60,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.ai_agents.belongsTo(db.websites, { + as: 'website', + foreignKey: { + name: 'websiteId', + }, + constraints: false, + }); + db.ai_agents.belongsTo(db.users, { as: 'createdBy', }); diff --git a/backend/src/db/models/organizations.js b/backend/src/db/models/organizations.js index 34c7f6b..fe8a0fb 100644 --- a/backend/src/db/models/organizations.js +++ b/backend/src/db/models/organizations.js @@ -58,6 +58,14 @@ module.exports = function (sequelize, DataTypes) { constraints: false, }); + db.organizations.hasMany(db.websites, { + as: 'websites_organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + //end loop db.organizations.belongsTo(db.users, { diff --git a/backend/src/db/models/websites.js b/backend/src/db/models/websites.js new file mode 100644 index 0000000..4c84dba --- /dev/null +++ b/backend/src/db/models/websites.js @@ -0,0 +1,73 @@ +const config = require('../../config'); +const providers = config.providers; +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const moment = require('moment'); + +module.exports = function (sequelize, DataTypes) { + const websites = sequelize.define( + 'websites', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + + name: { + type: DataTypes.TEXT, + }, + + url: { + type: DataTypes.TEXT, + }, + + description: { + type: DataTypes.TEXT, + }, + + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + websites.associate = (db) => { + /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity + + db.websites.hasMany(db.ai_agents, { + as: 'ai_agents_website', + foreignKey: { + name: 'websiteId', + }, + constraints: false, + }); + + //end loop + + db.websites.belongsTo(db.organizations, { + as: 'organizations', + foreignKey: { + name: 'organizationsId', + }, + constraints: false, + }); + + db.websites.belongsTo(db.users, { + as: 'createdBy', + }); + + db.websites.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return websites; +}; diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 8aad866..75ea12d 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -110,6 +110,7 @@ module.exports = { 'roles', 'permissions', 'organizations', + 'websites', , ]; await queryInterface.bulkInsert( @@ -547,6 +548,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_INTERACTIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_WEBSITES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_WEBSITES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_WEBSITES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_WEBSITES'), + }, + { createdAt, updatedAt, @@ -697,6 +723,31 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_ORGANIZATIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('CREATE_WEBSITES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('READ_WEBSITES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('UPDATE_WEBSITES'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('SuperAdmin'), + permissionId: getId('DELETE_WEBSITES'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index e507bbb..0b14ada 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -7,6 +7,8 @@ const Interactions = db.interactions; const Organizations = db.organizations; +const Websites = db.websites; + const AiAgentsData = [ { name: 'NavigatorBot', @@ -16,6 +18,8 @@ const AiAgentsData = [ description: 'A bot that navigates the website and provides information.', // type code here for "relation_one" field + + // type code here for "relation_one" field }, { @@ -26,6 +30,8 @@ const AiAgentsData = [ description: 'Retrieves information based on user queries.', // type code here for "relation_one" field + + // type code here for "relation_one" field }, { @@ -36,6 +42,20 @@ const AiAgentsData = [ description: 'Assists users with chat-based interactions.', // type code here for "relation_one" field + + // type code here for "relation_one" field + }, + + { + name: 'GuideBot', + + sitemap_url: 'https://example.com/sitemap.xml', + + description: 'Guides users through the website.', + + // type code here for "relation_one" field + + // type code here for "relation_one" field }, ]; @@ -81,19 +101,79 @@ const InteractionsData = [ // type code here for "relation_one" field }, + + { + // type code here for "relation_one" field + + // type code here for "relation_one" field + + interaction_start: new Date('2023-10-04T13:00:00Z'), + + interaction_end: new Date('2023-10-04T13:25:00Z'), + + transcript: 'User asked for support on a technical issue.', + + // type code here for "relation_one" field + }, ]; const OrganizationsData = [ { - name: 'Anton van Leeuwenhoek', + name: 'Willard Libby', }, { - name: 'Hans Selye', + name: 'Rudolf Virchow', }, { - name: 'Max Delbruck', + name: 'Max Born', + }, + + { + name: 'Sheldon Glashow', + }, +]; + +const WebsitesData = [ + { + // type code here for "relation_one" field + + name: 'Neils Bohr', + + url: 'Louis Victor de Broglie', + + description: 'Francis Galton', + }, + + { + // type code here for "relation_one" field + + name: 'Edward Teller', + + url: 'Charles Lyell', + + description: 'Tycho Brahe', + }, + + { + // type code here for "relation_one" field + + name: 'Theodosius Dobzhansky', + + url: 'George Gaylord Simpson', + + description: 'James Watson', + }, + + { + // type code here for "relation_one" field + + name: 'Francis Galton', + + url: 'B. F. Skinner', + + description: 'Hans Bethe', }, ]; @@ -132,6 +212,17 @@ async function associateUserWithOrganization() { if (User2?.setOrganization) { await User2.setOrganization(relatedOrganization2); } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const User3 = await Users.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (User3?.setOrganization) { + await User3.setOrganization(relatedOrganization3); + } } async function associateAiAgentWithOrganization() { @@ -167,6 +258,63 @@ async function associateAiAgentWithOrganization() { if (AiAgent2?.setOrganization) { await AiAgent2.setOrganization(relatedOrganization2); } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const AiAgent3 = await AiAgents.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (AiAgent3?.setOrganization) { + await AiAgent3.setOrganization(relatedOrganization3); + } +} + +async function associateAiAgentWithWebsite() { + const relatedWebsite0 = await Websites.findOne({ + offset: Math.floor(Math.random() * (await Websites.count())), + }); + const AiAgent0 = await AiAgents.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (AiAgent0?.setWebsite) { + await AiAgent0.setWebsite(relatedWebsite0); + } + + const relatedWebsite1 = await Websites.findOne({ + offset: Math.floor(Math.random() * (await Websites.count())), + }); + const AiAgent1 = await AiAgents.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (AiAgent1?.setWebsite) { + await AiAgent1.setWebsite(relatedWebsite1); + } + + const relatedWebsite2 = await Websites.findOne({ + offset: Math.floor(Math.random() * (await Websites.count())), + }); + const AiAgent2 = await AiAgents.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (AiAgent2?.setWebsite) { + await AiAgent2.setWebsite(relatedWebsite2); + } + + const relatedWebsite3 = await Websites.findOne({ + offset: Math.floor(Math.random() * (await Websites.count())), + }); + const AiAgent3 = await AiAgents.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (AiAgent3?.setWebsite) { + await AiAgent3.setWebsite(relatedWebsite3); + } } async function associateInteractionWithAgent() { @@ -202,6 +350,17 @@ async function associateInteractionWithAgent() { if (Interaction2?.setAgent) { await Interaction2.setAgent(relatedAgent2); } + + const relatedAgent3 = await AiAgents.findOne({ + offset: Math.floor(Math.random() * (await AiAgents.count())), + }); + const Interaction3 = await Interactions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Interaction3?.setAgent) { + await Interaction3.setAgent(relatedAgent3); + } } async function associateInteractionWithUser() { @@ -237,6 +396,17 @@ async function associateInteractionWithUser() { if (Interaction2?.setUser) { await Interaction2.setUser(relatedUser2); } + + const relatedUser3 = await Users.findOne({ + offset: Math.floor(Math.random() * (await Users.count())), + }); + const Interaction3 = await Interactions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Interaction3?.setUser) { + await Interaction3.setUser(relatedUser3); + } } async function associateInteractionWithOrganization() { @@ -272,6 +442,63 @@ async function associateInteractionWithOrganization() { if (Interaction2?.setOrganization) { await Interaction2.setOrganization(relatedOrganization2); } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Interaction3 = await Interactions.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Interaction3?.setOrganization) { + await Interaction3.setOrganization(relatedOrganization3); + } +} + +async function associateWebsiteWithOrganization() { + const relatedOrganization0 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Website0 = await Websites.findOne({ + order: [['id', 'ASC']], + offset: 0, + }); + if (Website0?.setOrganization) { + await Website0.setOrganization(relatedOrganization0); + } + + const relatedOrganization1 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Website1 = await Websites.findOne({ + order: [['id', 'ASC']], + offset: 1, + }); + if (Website1?.setOrganization) { + await Website1.setOrganization(relatedOrganization1); + } + + const relatedOrganization2 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Website2 = await Websites.findOne({ + order: [['id', 'ASC']], + offset: 2, + }); + if (Website2?.setOrganization) { + await Website2.setOrganization(relatedOrganization2); + } + + const relatedOrganization3 = await Organizations.findOne({ + offset: Math.floor(Math.random() * (await Organizations.count())), + }); + const Website3 = await Websites.findOne({ + order: [['id', 'ASC']], + offset: 3, + }); + if (Website3?.setOrganization) { + await Website3.setOrganization(relatedOrganization3); + } } module.exports = { @@ -282,6 +509,8 @@ module.exports = { await Organizations.bulkCreate(OrganizationsData); + await Websites.bulkCreate(WebsitesData); + await Promise.all([ // Similar logic for "relation_many" @@ -289,11 +518,15 @@ module.exports = { await associateAiAgentWithOrganization(), + await associateAiAgentWithWebsite(), + await associateInteractionWithAgent(), await associateInteractionWithUser(), await associateInteractionWithOrganization(), + + await associateWebsiteWithOrganization(), ]); }, @@ -303,5 +536,7 @@ module.exports = { await queryInterface.bulkDelete('interactions', null, {}); await queryInterface.bulkDelete('organizations', null, {}); + + await queryInterface.bulkDelete('websites', null, {}); }, }; diff --git a/backend/src/db/seeders/20250430204409.js b/backend/src/db/seeders/20250430204409.js new file mode 100644 index 0000000..c94b209 --- /dev/null +++ b/backend/src/db/seeders/20250430204409.js @@ -0,0 +1,87 @@ +const { v4: uuid } = require('uuid'); +const db = require('../models'); +const Sequelize = require('sequelize'); +const config = require('../../config'); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, + ]; + } + + const entities = ['websites']; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert('permissions', createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: { name: config.roles.super_admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/index.js b/backend/src/index.js index 50a3019..a76f1b6 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -33,6 +33,8 @@ const permissionsRoutes = require('./routes/permissions'); const organizationsRoutes = require('./routes/organizations'); +const websitesRoutes = require('./routes/websites'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -134,6 +136,12 @@ app.use( organizationsRoutes, ); +app.use( + '/api/websites', + passport.authenticate('jwt', { session: false }), + websitesRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/websites.js b/backend/src/routes/websites.js new file mode 100644 index 0000000..c33e2bc --- /dev/null +++ b/backend/src/routes/websites.js @@ -0,0 +1,458 @@ +const express = require('express'); + +const WebsitesService = require('../services/websites'); +const WebsitesDBApi = require('../db/api/websites'); +const wrapAsync = require('../helpers').wrapAsync; + +const config = require('../config'); + +const router = express.Router(); + +const { parse } = require('json2csv'); + +const { checkCrudPermissions } = require('../middlewares/check-permissions'); + +router.use(checkCrudPermissions('websites')); + +/** + * @swagger + * components: + * schemas: + * Websites: + * type: object + * properties: + + * name: + * type: string + * default: name + * url: + * type: string + * default: url + * description: + * type: string + * default: description + + */ + +/** + * @swagger + * tags: + * name: Websites + * description: The Websites managing API + */ + +/** + * @swagger + * /api/websites: + * post: + * security: + * - bearerAuth: [] + * tags: [Websites] + * 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/Websites" + * responses: + * 200: + * description: The item was successfully added + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Websites" + * 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 WebsitesService.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: [Websites] + * 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/Websites" + * responses: + * 200: + * description: The items were successfully imported + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Websites" + * 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 WebsitesService.bulkImport(req, res, true, link.host); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/websites/{id}: + * put: + * security: + * - bearerAuth: [] + * tags: [Websites] + * 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/Websites" + * required: + * - id + * responses: + * 200: + * description: The item data was successfully updated + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Websites" + * 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 WebsitesService.update(req.body.data, req.body.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/websites/{id}: + * delete: + * security: + * - bearerAuth: [] + * tags: [Websites] + * 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/Websites" + * 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 WebsitesService.remove(req.params.id, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/websites/deleteByIds: + * post: + * security: + * - bearerAuth: [] + * tags: [Websites] + * 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/Websites" + * 401: + * $ref: "#/components/responses/UnauthorizedError" + * 404: + * description: Items not found + * 500: + * description: Some server error + */ +router.post( + '/deleteByIds', + wrapAsync(async (req, res) => { + await WebsitesService.deleteByIds(req.body.data, req.currentUser); + const payload = true; + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/websites: + * get: + * security: + * - bearerAuth: [] + * tags: [Websites] + * summary: Get all websites + * description: Get all websites + * responses: + * 200: + * description: Websites list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Websites" + * 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 WebsitesDBApi.findAll(req.query, globalAccess, { + currentUser, + }); + if (filetype && filetype === 'csv') { + const fields = ['id', 'name', 'url', '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/websites/count: + * get: + * security: + * - bearerAuth: [] + * tags: [Websites] + * summary: Count all websites + * description: Count all websites + * responses: + * 200: + * description: Websites count successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Websites" + * 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 WebsitesDBApi.findAll(req.query, globalAccess, { + countOnly: true, + currentUser, + }); + + res.status(200).send(payload); + }), +); + +/** + * @swagger + * /api/websites/autocomplete: + * get: + * security: + * - bearerAuth: [] + * tags: [Websites] + * summary: Find all websites that match search criteria + * description: Find all websites that match search criteria + * responses: + * 200: + * description: Websites list successfully received + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Websites" + * 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 WebsitesDBApi.findAllAutocomplete( + req.query.query, + req.query.limit, + req.query.offset, + globalAccess, + organizationId, + ); + + res.status(200).send(payload); +}); + +/** + * @swagger + * /api/websites/{id}: + * get: + * security: + * - bearerAuth: [] + * tags: [Websites] + * 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/Websites" + * 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 WebsitesDBApi.findBy({ id: req.params.id }); + + res.status(200).send(payload); + }), +); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/services/search.js b/backend/src/services/search.js index 3676eb5..51653f3 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -48,6 +48,8 @@ module.exports = class SearchService { interactions: ['transcript'], organizations: ['name'], + + websites: ['name', 'url', 'description'], }; const columnsInt = {}; diff --git a/backend/src/services/websites.js b/backend/src/services/websites.js new file mode 100644 index 0000000..0532330 --- /dev/null +++ b/backend/src/services/websites.js @@ -0,0 +1,114 @@ +const db = require('../db/models'); +const WebsitesDBApi = require('../db/api/websites'); +const processFile = require('../middlewares/upload'); +const ValidationError = require('./notifications/errors/validation'); +const csv = require('csv-parser'); +const axios = require('axios'); +const config = require('../config'); +const stream = require('stream'); + +module.exports = class WebsitesService { + static async create(data, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + await WebsitesDBApi.create(data, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async bulkImport(req, res, sendInvitationEmails = true, host) { + const transaction = await db.sequelize.transaction(); + + try { + await processFile(req, res); + const bufferStream = new stream.PassThrough(); + const results = []; + + await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', async () => { + console.log('CSV results', results); + resolve(); + }) + .on('error', (error) => reject(error)); + }); + + await WebsitesDBApi.bulkImport(results, { + transaction, + ignoreDuplicates: true, + validate: true, + currentUser: req.currentUser, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async update(data, id, currentUser) { + const transaction = await db.sequelize.transaction(); + try { + let websites = await WebsitesDBApi.findBy({ id }, { transaction }); + + if (!websites) { + throw new ValidationError('websitesNotFound'); + } + + const updatedWebsites = await WebsitesDBApi.update(id, data, { + currentUser, + transaction, + }); + + await transaction.commit(); + return updatedWebsites; + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async deleteByIds(ids, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await WebsitesDBApi.deleteByIds(ids, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } + + static async remove(id, currentUser) { + const transaction = await db.sequelize.transaction(); + + try { + await WebsitesDBApi.remove(id, { + currentUser, + transaction, + }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/frontend/json/runtimeError.json b/frontend/json/runtimeError.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/frontend/json/runtimeError.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/components/Ai_agents/CardAi_agents.tsx b/frontend/src/components/Ai_agents/CardAi_agents.tsx index c9f8bd9..bd750d3 100644 --- a/frontend/src/components/Ai_agents/CardAi_agents.tsx +++ b/frontend/src/components/Ai_agents/CardAi_agents.tsx @@ -106,6 +106,17 @@ const CardAi_agents = ({ + +
+
+ Website +
+
+
+ {dataFormatter.websitesOneListFormatter(item.website)} +
+
+
))} diff --git a/frontend/src/components/Ai_agents/ListAi_agents.tsx b/frontend/src/components/Ai_agents/ListAi_agents.tsx index 54084e7..b363a17 100644 --- a/frontend/src/components/Ai_agents/ListAi_agents.tsx +++ b/frontend/src/components/Ai_agents/ListAi_agents.tsx @@ -69,6 +69,13 @@ const ListAi_agents = ({

Description

{item.description}

+ +
+

Website

+

+ {dataFormatter.websitesOneListFormatter(item.website)} +

+
value?.id, + getOptionLabel: (value: any) => value?.label, + valueOptions: await callOptionsApi('websites'), + valueGetter: (params: GridValueGetterParams) => + params?.value?.id ?? params?.value, + }, + { field: 'actions', type: 'actions', diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index 3833078..081158b 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -20,7 +20,7 @@ export default function WebSiteFooter({ const style = FooterStyle.WITH_PROJECT_NAME; - const design = FooterDesigns.DEFAULT_DESIGN; + const design = FooterDesigns.DESIGN_DIVERSITY; return (
state.style.websiteHeder); const borders = useAppSelector((state) => state.style.borders); - const style = HeaderStyle.PAGES_LEFT; + const style = HeaderStyle.PAGES_RIGHT; const design = HeaderDesigns.DESIGN_DIVERSITY; return ( diff --git a/frontend/src/components/Websites/CardWebsites.tsx b/frontend/src/components/Websites/CardWebsites.tsx new file mode 100644 index 0000000..973971a --- /dev/null +++ b/frontend/src/components/Websites/CardWebsites.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import ImageField from '../ImageField'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import dataFormatter from '../../helpers/dataFormatter'; +import { Pagination } from '../Pagination'; +import { saveFile } from '../../helpers/fileSaver'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + websites: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const CardWebsites = ({ + websites, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const asideScrollbarsStyle = useAppSelector( + (state) => state.style.asideScrollbarsStyle, + ); + const bgColor = useAppSelector((state) => state.style.cardsColor); + const darkMode = useAppSelector((state) => state.style.darkMode); + const corners = useAppSelector((state) => state.style.corners); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_WEBSITES'); + + return ( +
+ {loading && } +
    + {!loading && + websites.map((item, index) => ( +
  • +
    + + {item.name} + + +
    + +
    +
    +
    +
    +
    Name
    +
    +
    {item.name}
    +
    +
    + +
    +
    Url
    +
    +
    {item.url}
    +
    +
    + +
    +
    + Description +
    +
    +
    + {item.description} +
    +
    +
    +
    +
  • + ))} + {!loading && websites.length === 0 && ( +
    +

    No data to display

    +
    + )} +
+
+ +
+
+ ); +}; + +export default CardWebsites; diff --git a/frontend/src/components/Websites/ListWebsites.tsx b/frontend/src/components/Websites/ListWebsites.tsx new file mode 100644 index 0000000..a446f60 --- /dev/null +++ b/frontend/src/components/Websites/ListWebsites.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import CardBox from '../CardBox'; +import ImageField from '../ImageField'; +import dataFormatter from '../../helpers/dataFormatter'; +import { saveFile } from '../../helpers/fileSaver'; +import ListActionsPopover from '../ListActionsPopover'; +import { useAppSelector } from '../../stores/hooks'; +import { Pagination } from '../Pagination'; +import LoadingSpinner from '../LoadingSpinner'; +import Link from 'next/link'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Props = { + websites: any[]; + loading: boolean; + onDelete: (id: string) => void; + currentPage: number; + numPages: number; + onPageChange: (page: number) => void; +}; + +const ListWebsites = ({ + websites, + loading, + onDelete, + currentPage, + numPages, + onPageChange, +}: Props) => { + const currentUser = useAppSelector((state) => state.auth.currentUser); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_WEBSITES'); + + const corners = useAppSelector((state) => state.style.corners); + const bgColor = useAppSelector((state) => state.style.cardsColor); + + return ( + <> +
+ {loading && } + {!loading && + websites.map((item) => ( + +
+ dark:divide-dark-700 overflow-x-auto' + } + > +
+

Name

+

{item.name}

+
+ +
+

Url

+

{item.url}

+
+ +
+

Description

+

{item.description}

+
+ + +
+
+ ))} + {!loading && websites.length === 0 && ( +
+

No data to display

+
+ )} +
+
+ +
+ + ); +}; + +export default ListWebsites; diff --git a/frontend/src/components/Websites/TableWebsites.tsx b/frontend/src/components/Websites/TableWebsites.tsx new file mode 100644 index 0000000..662255b --- /dev/null +++ b/frontend/src/components/Websites/TableWebsites.tsx @@ -0,0 +1,481 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import BaseButton from '../BaseButton'; +import CardBoxModal from '../CardBoxModal'; +import CardBox from '../CardBox'; +import { + fetch, + update, + deleteItem, + setRefetch, + deleteItemsByIds, +} from '../../stores/websites/websitesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { Field, Form, Formik } from 'formik'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { loadColumns } from './configureWebsitesCols'; +import _ from 'lodash'; +import dataFormatter from '../../helpers/dataFormatter'; +import { dataGridStyles } from '../../styles'; + +const perPage = 10; + +const TableSampleWebsites = ({ + filterItems, + setFilterItems, + filters, + showGrid, +}) => { + const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); + + const dispatch = useAppDispatch(); + const router = useRouter(); + + const pagesList = []; + const [id, setId] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [filterRequest, setFilterRequest] = React.useState(''); + const [columns, setColumns] = useState([]); + const [selectedRows, setSelectedRows] = useState([]); + const [sortModel, setSortModel] = useState([ + { + field: '', + sort: 'desc', + }, + ]); + + const { + websites, + loading, + count, + notify: websitesNotify, + refetch, + } = useAppSelector((state) => state.websites); + const { currentUser } = useAppSelector((state) => state.auth); + const focusRing = useAppSelector((state) => state.style.focusRingColor); + const bgColor = useAppSelector((state) => state.style.bgLayoutColor); + const corners = useAppSelector((state) => state.style.corners); + const numPages = + Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); + for (let i = 0; i < numPages; i++) { + pagesList.push(i); + } + + const loadData = async (page = currentPage, request = filterRequest) => { + if (page !== currentPage) setCurrentPage(page); + if (request !== filterRequest) setFilterRequest(request); + const { sort, field } = sortModel[0]; + + const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; + dispatch(fetch({ limit: perPage, page, query })); + }; + + useEffect(() => { + if (websitesNotify.showNotification) { + notify(websitesNotify.typeNotification, websitesNotify.textNotification); + } + }, [websitesNotify.showNotification]); + + useEffect(() => { + if (!currentUser) return; + loadData(); + }, [sortModel, currentUser]); + + useEffect(() => { + if (refetch) { + loadData(0); + dispatch(setRefetch(false)); + } + }, [refetch, dispatch]); + + const [isModalInfoActive, setIsModalInfoActive] = useState(false); + const [isModalTrashActive, setIsModalTrashActive] = useState(false); + + const handleModalAction = () => { + setIsModalInfoActive(false); + setIsModalTrashActive(false); + }; + + const handleDeleteModalAction = (id: string) => { + setId(id); + setIsModalTrashActive(true); + }; + const handleDeleteAction = async () => { + if (id) { + await dispatch(deleteItem(id)); + await loadData(0); + setIsModalTrashActive(false); + } + }; + + const generateFilterRequests = useMemo(() => { + let request = '&'; + filterItems.forEach((item) => { + const isRangeFilter = filters.find( + (filter) => + filter.title === item.fields.selectedField && + (filter.number || filter.date), + ); + + if (isRangeFilter) { + const from = item.fields.filterValueFrom; + const to = item.fields.filterValueTo; + if (from) { + request += `${item.fields.selectedField}Range=${from}&`; + } + if (to) { + request += `${item.fields.selectedField}Range=${to}&`; + } + } else { + const value = item.fields.filterValue; + if (value) { + request += `${item.fields.selectedField}=${value}&`; + } + } + }); + return request; + }, [filterItems, filters]); + + const deleteFilter = (value) => { + const newItems = filterItems.filter((item) => item.id !== value); + + if (newItems.length) { + setFilterItems(newItems); + } else { + loadData(0, ''); + + setFilterItems(newItems); + } + }; + + const handleSubmit = () => { + loadData(0, generateFilterRequests); + }; + + const handleChange = (id) => (e) => { + const value = e.target.value; + const name = e.target.name; + + setFilterItems( + filterItems.map((item) => { + if (item.id !== id) return item; + if (name === 'selectedField') return { id, fields: { [name]: value } }; + + return { id, fields: { ...item.fields, [name]: value } }; + }), + ); + }; + + const handleReset = () => { + setFilterItems([]); + loadData(0, ''); + }; + + const onPageChange = (page: number) => { + loadData(page); + setCurrentPage(page); + }; + + useEffect(() => { + if (!currentUser) return; + + loadColumns(handleDeleteModalAction, `websites`, currentUser).then( + (newCols) => setColumns(newCols), + ); + }, [currentUser]); + + const handleTableSubmit = async (id: string, data) => { + if (!_.isEmpty(data)) { + await dispatch(update({ id, data })) + .unwrap() + .then((res) => res) + .catch((err) => { + throw new Error(err); + }); + } + }; + + const onDeleteRows = async (selectedRows) => { + await dispatch(deleteItemsByIds(selectedRows)); + await loadData(0); + }; + + const controlClasses = + 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + + ` ${bgColor} ${focusRing} ${corners} ` + + 'dark:bg-slate-800 border'; + + const dataGrid = ( +
+ `datagrid--row`} + rows={websites ?? []} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10, + }, + }, + }} + disableRowSelectionOnClick + onProcessRowUpdateError={(params) => { + console.log('Error', params); + }} + processRowUpdate={async (newRow, oldRow) => { + const data = dataFormatter.dataGridEditFormatter(newRow); + + try { + await handleTableSubmit(newRow.id, data); + return newRow; + } catch { + return oldRow; + } + }} + sortingMode={'server'} + checkboxSelection + onRowSelectionModelChange={(ids) => { + setSelectedRows(ids); + }} + onSortModelChange={(params) => { + params.length + ? setSortModel(params) + : setSortModel([{ field: '', sort: 'desc' }]); + }} + rowCount={count} + pageSizeOptions={[10]} + paginationMode={'server'} + loading={loading} + onPaginationModelChange={(params) => { + onPageChange(params.page); + }} + /> +
+ ); + + return ( + <> + {filterItems && Array.isArray(filterItems) && filterItems.length ? ( + + null} + > +
+ <> + {filterItems && + filterItems.map((filterItem) => { + return ( +
+
+
+ Filter +
+ + {filters.map((selectOption) => ( + + ))} + +
+ {filters.find( + (filter) => + filter.title === filterItem?.fields?.selectedField, + )?.type === 'enum' ? ( +
+
Value
+ + + {filters + .find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + ) + ?.options?.map((option) => ( + + ))} + +
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.number ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : filters.find( + (filter) => + filter.title === + filterItem?.fields?.selectedField, + )?.date ? ( +
+
+
+ From +
+ +
+
+
+ To +
+ +
+
+ ) : ( +
+
+ Contains +
+ +
+ )} +
+
+ Action +
+ { + deleteFilter(filterItem.id); + }} + /> +
+
+ ); + })} +
+ + +
+ +
+
+
+ ) : null} + +

Are you sure you want to delete this item?

+
+ + {dataGrid} + + {selectedRows.length > 0 && + createPortal( + onDeleteRows(selectedRows)} + />, + document.getElementById('delete-rows-button'), + )} + + + ); +}; + +export default TableSampleWebsites; diff --git a/frontend/src/components/Websites/configureWebsitesCols.tsx b/frontend/src/components/Websites/configureWebsitesCols.tsx new file mode 100644 index 0000000..302f3ca --- /dev/null +++ b/frontend/src/components/Websites/configureWebsitesCols.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_WEBSITES'); + + return [ + { + field: 'name', + headerName: 'Name', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'url', + headerName: 'Url', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'description', + headerName: 'Description', + flex: 1, + minWidth: 120, + filterable: false, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + + editable: hasUpdatePermission, + }, + + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ + , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/WidgetCreator/WidgetCreator.tsx b/frontend/src/components/WidgetCreator/WidgetCreator.tsx index ff6a3e5..0ac67c9 100644 --- a/frontend/src/components/WidgetCreator/WidgetCreator.tsx +++ b/frontend/src/components/WidgetCreator/WidgetCreator.tsx @@ -53,7 +53,7 @@ export const WidgetCreator = ({ resetForm: any, ) => { const description = values.description; - const projectId = '31127'; + const projectId = ''; const payload = { roleId: widgetsRole?.role?.value, diff --git a/frontend/src/helpers/dataFormatter.js b/frontend/src/helpers/dataFormatter.js index 97dfd6b..b059857 100644 --- a/frontend/src/helpers/dataFormatter.js +++ b/frontend/src/helpers/dataFormatter.js @@ -133,4 +133,23 @@ export default { if (!val) return ''; return { label: val.name, id: val.id }; }, + + websitesManyListFormatter(val) { + if (!val || !val.length) return []; + return val.map((item) => item.name); + }, + websitesOneListFormatter(val) { + if (!val) return ''; + return val.name; + }, + websitesManyListFormatterEdit(val) { + if (!val || !val.length) return []; + return val.map((item) => { + return { id: item.id, label: item.name }; + }); + }, + websitesOneListFormatterEdit(val) { + if (!val) return ''; + return { label: val.name, id: val.id }; + }, }; diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index d982e95..e1365a5 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -62,6 +62,14 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiTable ?? icon.mdiTable, permissions: 'READ_ORGANIZATIONS', }, + { + href: '/websites/websites-list', + label: 'Websites', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_WEBSITES', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/ai_agents/[ai_agentsId].tsx b/frontend/src/pages/ai_agents/[ai_agentsId].tsx index 0dbc683..2a33cf6 100644 --- a/frontend/src/pages/ai_agents/[ai_agentsId].tsx +++ b/frontend/src/pages/ai_agents/[ai_agentsId].tsx @@ -45,6 +45,8 @@ const EditAi_agents = () => { description: '', organizations: null, + + website: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -128,6 +130,17 @@ const EditAi_agents = () => { > + + + + diff --git a/frontend/src/pages/ai_agents/ai_agents-edit.tsx b/frontend/src/pages/ai_agents/ai_agents-edit.tsx index 3bedd83..29a6520 100644 --- a/frontend/src/pages/ai_agents/ai_agents-edit.tsx +++ b/frontend/src/pages/ai_agents/ai_agents-edit.tsx @@ -45,6 +45,8 @@ const EditAi_agentsPage = () => { description: '', organizations: null, + + website: null, }; const [initialValues, setInitialValues] = useState(initVals); @@ -126,6 +128,17 @@ const EditAi_agentsPage = () => { > + + + + diff --git a/frontend/src/pages/ai_agents/ai_agents-list.tsx b/frontend/src/pages/ai_agents/ai_agents-list.tsx index 9e79a30..c5c4014 100644 --- a/frontend/src/pages/ai_agents/ai_agents-list.tsx +++ b/frontend/src/pages/ai_agents/ai_agents-list.tsx @@ -32,6 +32,8 @@ const Ai_agentsTablesPage = () => { { label: 'AgentName', title: 'name' }, { label: 'SitemapURL', title: 'sitemap_url' }, { label: 'Description', title: 'description' }, + + { label: 'Website', title: 'website' }, ]); const hasCreatePermission = diff --git a/frontend/src/pages/ai_agents/ai_agents-new.tsx b/frontend/src/pages/ai_agents/ai_agents-new.tsx index e892323..1bc6c5a 100644 --- a/frontend/src/pages/ai_agents/ai_agents-new.tsx +++ b/frontend/src/pages/ai_agents/ai_agents-new.tsx @@ -40,6 +40,8 @@ const initialValues = { description: '', organizations: '', + + website: '', }; const Ai_agentsNew = () => { @@ -95,6 +97,16 @@ const Ai_agentsNew = () => { > + + + + diff --git a/frontend/src/pages/ai_agents/ai_agents-table.tsx b/frontend/src/pages/ai_agents/ai_agents-table.tsx index 0c045ad..c8ee841 100644 --- a/frontend/src/pages/ai_agents/ai_agents-table.tsx +++ b/frontend/src/pages/ai_agents/ai_agents-table.tsx @@ -32,6 +32,8 @@ const Ai_agentsTablesPage = () => { { label: 'AgentName', title: 'name' }, { label: 'SitemapURL', title: 'sitemap_url' }, { label: 'Description', title: 'description' }, + + { label: 'Website', title: 'website' }, ]); const hasCreatePermission = diff --git a/frontend/src/pages/ai_agents/ai_agents-view.tsx b/frontend/src/pages/ai_agents/ai_agents-view.tsx index 4a6bcee..cc27f3e 100644 --- a/frontend/src/pages/ai_agents/ai_agents-view.tsx +++ b/frontend/src/pages/ai_agents/ai_agents-view.tsx @@ -83,6 +83,12 @@ const Ai_agentsView = () => {

{ai_agents?.organizations?.name ?? 'No data'}

+
+

Website

+ +

{ai_agents?.website?.name ?? 'No data'}

+
+ <>

Interactions Agent

{ const [roles, setRoles] = React.useState('Loading...'); const [permissions, setPermissions] = React.useState('Loading...'); const [organizations, setOrganizations] = React.useState('Loading...'); + const [websites, setWebsites] = React.useState('Loading...'); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -47,6 +48,7 @@ const Dashboard = () => { 'roles', 'permissions', 'organizations', + 'websites', ]; const fns = [ setUsers, @@ -55,6 +57,7 @@ const Dashboard = () => { setRoles, setPermissions, setOrganizations, + setWebsites, ]; const requests = entities.map((entity, index) => { @@ -362,6 +365,38 @@ const Dashboard = () => { )} + + {hasPermission(currentUser, 'READ_WEBSITES') && ( + +
+
+
+
+ Websites +
+
+ {websites} +
+
+
+ +
+
+
+ + )} diff --git a/frontend/src/pages/organizations/organizations-view.tsx b/frontend/src/pages/organizations/organizations-view.tsx index 7cbc385..4613e35 100644 --- a/frontend/src/pages/organizations/organizations-view.tsx +++ b/frontend/src/pages/organizations/organizations-view.tsx @@ -208,6 +208,51 @@ const OrganizationsView = () => {
+ <> +

Websites organizations

+ +
+ + + + + + + + + + + + {organizations.websites_organizations && + Array.isArray(organizations.websites_organizations) && + organizations.websites_organizations.map((item: any) => ( + + router.push( + `/websites/websites-view/?id=${item.id}`, + ) + } + > + + + + + + + ))} + +
NameUrlDescription
{item.name}{item.url}{item.description}
+
+ {!organizations?.websites_organizations?.length && ( +
No data
+ )} +
+ + { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + organizations: null, + + name: '', + + url: '', + + description: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { websites } = useAppSelector((state) => state.websites); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { websitesId } = router.query; + + useEffect(() => { + dispatch(fetch({ id: websitesId })); + }, [websitesId]); + + useEffect(() => { + if (typeof websites === 'object') { + setInitialValues(websites); + } + }, [websites]); + + useEffect(() => { + if (typeof websites === 'object') { + const newInitialVal = { ...initVals }; + + Object.keys(initVals).forEach((el) => (newInitialVal[el] = websites[el])); + + setInitialValues(newInitialVal); + } + }, [websites]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: websitesId, data })); + await router.push('/websites/websites-list'); + }; + + return ( + <> + + {getPageTitle('Edit websites')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/websites/websites-list')} + /> + + +
+
+
+ + ); +}; + +EditWebsites.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditWebsites; diff --git a/frontend/src/pages/websites/websites-edit.tsx b/frontend/src/pages/websites/websites-edit.tsx new file mode 100644 index 0000000..7735008 --- /dev/null +++ b/frontend/src/pages/websites/websites-edit.tsx @@ -0,0 +1,151 @@ +import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement, useEffect, useState } from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; + +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { SwitchField } from '../../components/SwitchField'; +import { RichTextField } from '../../components/RichTextField'; + +import { update, fetch } from '../../stores/websites/websitesSlice'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const EditWebsitesPage = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const initVals = { + organizations: null, + + name: '', + + url: '', + + description: '', + }; + const [initialValues, setInitialValues] = useState(initVals); + + const { websites } = useAppSelector((state) => state.websites); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + useEffect(() => { + dispatch(fetch({ id: id })); + }, [id]); + + useEffect(() => { + if (typeof websites === 'object') { + setInitialValues(websites); + } + }, [websites]); + + useEffect(() => { + if (typeof websites === 'object') { + const newInitialVal = { ...initVals }; + Object.keys(initVals).forEach((el) => (newInitialVal[el] = websites[el])); + setInitialValues(newInitialVal); + } + }, [websites]); + + const handleSubmit = async (data) => { + await dispatch(update({ id: id, data })); + await router.push('/websites/websites-list'); + }; + + return ( + <> + + {getPageTitle('Edit websites')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/websites/websites-list')} + /> + + +
+
+
+ + ); +}; + +EditWebsitesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default EditWebsitesPage; diff --git a/frontend/src/pages/websites/websites-list.tsx b/frontend/src/pages/websites/websites-list.tsx new file mode 100644 index 0000000..faa02af --- /dev/null +++ b/frontend/src/pages/websites/websites-list.tsx @@ -0,0 +1,166 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableWebsites from '../../components/Websites/TableWebsites'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/websites/websitesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const WebsitesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Url', title: 'url' }, + { label: 'Description', title: 'description' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_WEBSITES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getWebsitesCSV = async () => { + const response = await axios({ + url: '/websites?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'websitesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Websites')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + + +
+ + + + + ); +}; + +WebsitesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default WebsitesTablesPage; diff --git a/frontend/src/pages/websites/websites-new.tsx b/frontend/src/pages/websites/websites-new.tsx new file mode 100644 index 0000000..d69dfef --- /dev/null +++ b/frontend/src/pages/websites/websites-new.tsx @@ -0,0 +1,122 @@ +import { + mdiAccount, + mdiChartTimelineVariant, + mdiMail, + mdiUpload, +} from '@mdi/js'; +import Head from 'next/head'; +import React, { ReactElement } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; + +import { Field, Form, Formik } from 'formik'; +import FormField from '../../components/FormField'; +import BaseDivider from '../../components/BaseDivider'; +import BaseButtons from '../../components/BaseButtons'; +import BaseButton from '../../components/BaseButton'; +import FormCheckRadio from '../../components/FormCheckRadio'; +import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; +import FormFilePicker from '../../components/FormFilePicker'; +import FormImagePicker from '../../components/FormImagePicker'; +import { SwitchField } from '../../components/SwitchField'; + +import { SelectField } from '../../components/SelectField'; +import { SelectFieldMany } from '../../components/SelectFieldMany'; +import { RichTextField } from '../../components/RichTextField'; + +import { create } from '../../stores/websites/websitesSlice'; +import { useAppDispatch } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import moment from 'moment'; + +const initialValues = { + organizations: '', + + name: '', + + url: '', + + description: '', +}; + +const WebsitesNew = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + + const handleSubmit = async (data) => { + await dispatch(create(data)); + await router.push('/websites/websites-list'); + }; + return ( + <> + + {getPageTitle('New Item')} + + + + {''} + + + handleSubmit(values)} + > +
+ + + + + + + + + + + + + + + + + + + + + router.push('/websites/websites-list')} + /> + + +
+
+
+ + ); +}; + +WebsitesNew.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default WebsitesNew; diff --git a/frontend/src/pages/websites/websites-table.tsx b/frontend/src/pages/websites/websites-table.tsx new file mode 100644 index 0000000..2b132b5 --- /dev/null +++ b/frontend/src/pages/websites/websites-table.tsx @@ -0,0 +1,165 @@ +import { mdiChartTimelineVariant } from '@mdi/js'; +import Head from 'next/head'; +import { uniqueId } from 'lodash'; +import React, { ReactElement, useState } from 'react'; +import CardBox from '../../components/CardBox'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import SectionMain from '../../components/SectionMain'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import { getPageTitle } from '../../config'; +import TableWebsites from '../../components/Websites/TableWebsites'; +import BaseButton from '../../components/BaseButton'; +import axios from 'axios'; +import Link from 'next/link'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import CardBoxModal from '../../components/CardBoxModal'; +import DragDropFilePicker from '../../components/DragDropFilePicker'; +import { setRefetch, uploadCsv } from '../../stores/websites/websitesSlice'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const WebsitesTablesPage = () => { + const [filterItems, setFilterItems] = useState([]); + const [csvFile, setCsvFile] = useState(null); + const [isModalActive, setIsModalActive] = useState(false); + const [showTableView, setShowTableView] = useState(false); + + const { currentUser } = useAppSelector((state) => state.auth); + + const dispatch = useAppDispatch(); + + const [filters] = useState([ + { label: 'Name', title: 'name' }, + { label: 'Url', title: 'url' }, + { label: 'Description', title: 'description' }, + ]); + + const hasCreatePermission = + currentUser && hasPermission(currentUser, 'CREATE_WEBSITES'); + + const addFilter = () => { + const newItem = { + id: uniqueId(), + fields: { + filterValue: '', + filterValueFrom: '', + filterValueTo: '', + selectedField: '', + }, + }; + newItem.fields.selectedField = filters[0].title; + setFilterItems([...filterItems, newItem]); + }; + + const getWebsitesCSV = async () => { + const response = await axios({ + url: '/websites?filetype=csv', + method: 'GET', + responseType: 'blob', + }); + const type = response.headers['content-type']; + const blob = new Blob([response.data], { type: type }); + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = 'websitesCSV.csv'; + link.click(); + }; + + const onModalConfirm = async () => { + if (!csvFile) return; + await dispatch(uploadCsv(csvFile)); + dispatch(setRefetch(true)); + setCsvFile(null); + setIsModalActive(false); + }; + + const onModalCancel = () => { + setCsvFile(null); + setIsModalActive(false); + }; + + return ( + <> + + {getPageTitle('Websites')} + + + + {''} + + + {hasCreatePermission && ( + + )} + + + + + {hasCreatePermission && ( + setIsModalActive(true)} + /> + )} + +
+
+
+
+ + + +
+ + + + + ); +}; + +WebsitesTablesPage.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default WebsitesTablesPage; diff --git a/frontend/src/pages/websites/websites-view.tsx b/frontend/src/pages/websites/websites-view.tsx new file mode 100644 index 0000000..f7968a6 --- /dev/null +++ b/frontend/src/pages/websites/websites-view.tsx @@ -0,0 +1,144 @@ +import React, { ReactElement, useEffect } from 'react'; +import Head from 'next/head'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import { useRouter } from 'next/router'; +import { fetch } from '../../stores/websites/websitesSlice'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import ImageField from '../../components/ImageField'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; +import SectionMain from '../../components/SectionMain'; +import CardBox from '../../components/CardBox'; +import BaseButton from '../../components/BaseButton'; +import BaseDivider from '../../components/BaseDivider'; +import { mdiChartTimelineVariant } from '@mdi/js'; +import { SwitchField } from '../../components/SwitchField'; +import FormField from '../../components/FormField'; + +import { hasPermission } from '../../helpers/userPermissions'; + +const WebsitesView = () => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { websites } = useAppSelector((state) => state.websites); + + const { currentUser } = useAppSelector((state) => state.auth); + + const { id } = router.query; + + function removeLastCharacter(str) { + console.log(str, `str`); + return str.slice(0, -1); + } + + useEffect(() => { + dispatch(fetch({ id })); + }, [dispatch, id]); + + return ( + <> + + {getPageTitle('View websites')} + + + + + + +
+

organizations

+ +

{websites?.organizations?.name ?? 'No data'}

+
+ +
+

Name

+

{websites?.name}

+
+ +
+

Url

+

{websites?.url}

+
+ +
+

Description

+

{websites?.description}

+
+ + <> +

Ai_agents Website

+ +
+ + + + + + + + + + {websites.ai_agents_website && + Array.isArray(websites.ai_agents_website) && + websites.ai_agents_website.map((item: any) => ( + + router.push( + `/ai_agents/ai_agents-view/?id=${item.id}`, + ) + } + > + + + + + ))} + +
AgentNameSitemapURL
{item.name}{item.sitemap_url}
+
+ {!websites?.ai_agents_website?.length && ( +
No data
+ )} +
+ + + + + router.push('/websites/websites-list')} + /> +
+
+ + ); +}; + +WebsitesView.getLayout = function getLayout(page: ReactElement) { + return ( + + {page} + + ); +}; + +export default WebsitesView; diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts index 8159283..1ce7286 100644 --- a/frontend/src/stores/store.ts +++ b/frontend/src/stores/store.ts @@ -10,6 +10,7 @@ import interactionsSlice from './interactions/interactionsSlice'; import rolesSlice from './roles/rolesSlice'; import permissionsSlice from './permissions/permissionsSlice'; import organizationsSlice from './organizations/organizationsSlice'; +import websitesSlice from './websites/websitesSlice'; export const store = configureStore({ reducer: { @@ -24,6 +25,7 @@ export const store = configureStore({ roles: rolesSlice, permissions: permissionsSlice, organizations: organizationsSlice, + websites: websitesSlice, }, }); diff --git a/frontend/src/stores/websites/websitesSlice.ts b/frontend/src/stores/websites/websitesSlice.ts new file mode 100644 index 0000000..5755d44 --- /dev/null +++ b/frontend/src/stores/websites/websitesSlice.ts @@ -0,0 +1,236 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import axios from 'axios'; +import { + fulfilledNotify, + rejectNotify, + resetNotify, +} from '../../helpers/notifyStateHandler'; + +interface MainState { + websites: any; + loading: boolean; + count: number; + refetch: boolean; + rolesWidgets: any[]; + notify: { + showNotification: boolean; + textNotification: string; + typeNotification: string; + }; +} + +const initialState: MainState = { + websites: [], + loading: false, + count: 0, + refetch: false, + rolesWidgets: [], + notify: { + showNotification: false, + textNotification: '', + typeNotification: 'warn', + }, +}; + +export const fetch = createAsyncThunk('websites/fetch', async (data: any) => { + const { id, query } = data; + const result = await axios.get(`websites${query || (id ? `/${id}` : '')}`); + return id + ? result.data + : { rows: result.data.rows, count: result.data.count }; +}); + +export const deleteItemsByIds = createAsyncThunk( + 'websites/deleteByIds', + async (data: any, { rejectWithValue }) => { + try { + await axios.post('websites/deleteByIds', { data }); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const deleteItem = createAsyncThunk( + 'websites/deleteWebsites', + async (id: string, { rejectWithValue }) => { + try { + await axios.delete(`websites/${id}`); + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const create = createAsyncThunk( + 'websites/createWebsites', + async (data: any, { rejectWithValue }) => { + try { + const result = await axios.post('websites', { data }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const uploadCsv = createAsyncThunk( + 'websites/uploadCsv', + async (file: File, { rejectWithValue }) => { + try { + const data = new FormData(); + data.append('file', file); + data.append('filename', file.name); + + const result = await axios.post('websites/bulk-import', data, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const update = createAsyncThunk( + 'websites/updateWebsites', + async (payload: any, { rejectWithValue }) => { + try { + const result = await axios.put(`websites/${payload.id}`, { + id: payload.id, + data: payload.data, + }); + return result.data; + } catch (error) { + if (!error.response) { + throw error; + } + + return rejectWithValue(error.response.data); + } + }, +); + +export const websitesSlice = createSlice({ + name: 'websites', + initialState, + reducers: { + setRefetch: (state, action: PayloadAction) => { + state.refetch = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addCase(fetch.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(fetch.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(fetch.fulfilled, (state, action) => { + if (action.payload.rows && action.payload.count >= 0) { + state.websites = action.payload.rows; + state.count = action.payload.count; + } else { + state.websites = action.payload; + } + state.loading = false; + }); + + builder.addCase(deleteItemsByIds.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItemsByIds.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Websites has been deleted'); + }); + + builder.addCase(deleteItemsByIds.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(deleteItem.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + + builder.addCase(deleteItem.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Websites'.slice(0, -1)} has been deleted`); + }); + + builder.addCase(deleteItem.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(create.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(create.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Websites'.slice(0, -1)} has been created`); + }); + + builder.addCase(update.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(update.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, `${'Websites'.slice(0, -1)} has been updated`); + }); + builder.addCase(update.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + + builder.addCase(uploadCsv.pending, (state) => { + state.loading = true; + resetNotify(state); + }); + builder.addCase(uploadCsv.fulfilled, (state) => { + state.loading = false; + fulfilledNotify(state, 'Websites has been uploaded'); + }); + builder.addCase(uploadCsv.rejected, (state, action) => { + state.loading = false; + rejectNotify(state, action); + }); + }, +}); + +// Action creators are generated for each case reducer function +export const { setRefetch } = websitesSlice.actions; + +export default websitesSlice.reducer;