From 04519d1b0a086a31d918d9453971880d69fd26e3 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 29 May 2025 14:57:02 +0530 Subject: [PATCH 01/45] Added migrations for component_permissions and component_users table --- ...748509644056-CreateComponentPermissions.ts | 51 +++++++++++++ .../1748509665915-CreateComponentUsers.ts | 76 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 server/migrations/1748509644056-CreateComponentPermissions.ts create mode 100644 server/migrations/1748509665915-CreateComponentUsers.ts diff --git a/server/migrations/1748509644056-CreateComponentPermissions.ts b/server/migrations/1748509644056-CreateComponentPermissions.ts new file mode 100644 index 0000000000..baa66f01de --- /dev/null +++ b/server/migrations/1748509644056-CreateComponentPermissions.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class CreateComponentPermissions1748509644056 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'component_permissions', + columns: [ + { + name: 'id', + type: 'uuid', + isGenerated: true, + default: 'gen_random_uuid()', + isPrimary: true, + }, + { + name: 'component_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'type', + type: 'enum', + enum: ['SINGLE', 'GROUP'], + }, + { + name: 'created_at', + type: 'timestamp', + isNullable: false, + default: 'now()', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'component_permissions', + new TableForeignKey({ + columnNames: ['component_id'], + referencedColumnNames: ['id'], + referencedTableName: 'components', + onDelete: 'CASCADE', + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('component_permissions'); + } +} diff --git a/server/migrations/1748509665915-CreateComponentUsers.ts b/server/migrations/1748509665915-CreateComponentUsers.ts new file mode 100644 index 0000000000..dbc4e3ef45 --- /dev/null +++ b/server/migrations/1748509665915-CreateComponentUsers.ts @@ -0,0 +1,76 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class CreateComponentUsers1748509665915 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'component_users', + columns: [ + { + name: 'id', + type: 'uuid', + isGenerated: true, + default: 'gen_random_uuid()', + isPrimary: true, + }, + { + name: 'component_permissions_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'user_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'permission_groups_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamp', + isNullable: false, + default: 'now()', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'component_users', + new TableForeignKey({ + columnNames: ['component_permissions_id'], + referencedColumnNames: ['id'], + referencedTableName: 'component_permissions', + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'component_users', + new TableForeignKey({ + columnNames: ['user_id'], + referencedColumnNames: ['id'], + referencedTableName: 'users', + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'component_users', + new TableForeignKey({ + columnNames: ['permission_groups_id'], + referencedColumnNames: ['id'], + referencedTableName: 'permission_groups', + onDelete: 'CASCADE', + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('component_users'); + } +} From 2e3f52335ada81d7f23146b86ae604e363528b9e Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 29 May 2025 17:17:24 +0530 Subject: [PATCH 02/45] Added the required entities for component_permissions and component_users tables --- server/src/entities/component.entity.ts | 4 +++ .../entities/component_permissions.entity.ts | 29 ++++++++++++++++ server/src/entities/component_users.entity.ts | 34 +++++++++++++++++++ .../src/entities/group_permissions.entity.ts | 4 +++ server/src/entities/user.entity.ts | 4 +++ 5 files changed, 75 insertions(+) create mode 100644 server/src/entities/component_permissions.entity.ts create mode 100644 server/src/entities/component_users.entity.ts diff --git a/server/src/entities/component.entity.ts b/server/src/entities/component.entity.ts index bb22c7dcd4..6ef8d20b0c 100644 --- a/server/src/entities/component.entity.ts +++ b/server/src/entities/component.entity.ts @@ -11,6 +11,7 @@ import { } from 'typeorm'; import { Page } from './page.entity'; import { Layout } from './layout.entity'; +import { ComponentPermission } from './component_permissions.entity'; @Entity({ name: 'components' }) @Index('idx_component_page_id', ['pageId']) @@ -60,4 +61,7 @@ export class Component { @OneToMany(() => Layout, (layout) => layout.component) layouts: Layout[]; + + @OneToMany(() => ComponentPermission, (permission) => permission.component) + permissions: ComponentPermission[]; } diff --git a/server/src/entities/component_permissions.entity.ts b/server/src/entities/component_permissions.entity.ts new file mode 100644 index 0000000000..9ed804eb4e --- /dev/null +++ b/server/src/entities/component_permissions.entity.ts @@ -0,0 +1,29 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, OneToMany } from 'typeorm'; +import { Component } from './component.entity'; +import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants'; +import { ComponentUser } from './component_users.entity'; + +@Entity('component_permissions') +export class ComponentPermission { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'component_id', type: 'uuid', nullable: false }) + componentId: string; + + @Column({ + type: 'enum', + enum: PAGE_PERMISSION_TYPE, + }) + type: PAGE_PERMISSION_TYPE; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @ManyToOne(() => Component, (component) => component.permissions, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'component_id' }) + component: Component; + + @OneToMany(() => ComponentUser, (componentUser) => componentUser.componentPermission) + users: ComponentUser[]; +} diff --git a/server/src/entities/component_users.entity.ts b/server/src/entities/component_users.entity.ts new file mode 100644 index 0000000000..2728297e3b --- /dev/null +++ b/server/src/entities/component_users.entity.ts @@ -0,0 +1,34 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm'; +import { User } from './user.entity'; +import { ComponentPermission } from './component_permissions.entity'; +import { GroupPermissions } from './group_permissions.entity'; + +@Entity('component_users') +export class ComponentUser { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'component_permissions_id', type: 'uuid' }) + componentPermissionsId: string; + + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + userId: string | null; + + @Column({ name: 'permission_groups_id', type: 'uuid', nullable: true }) + permissionGroupsId: string | null; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @ManyToOne(() => ComponentPermission, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'component_permissions_id' }) + componentPermission: ComponentPermission; + + @ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => GroupPermissions, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'permission_groups_id' }) + permissionGroup: GroupPermissions; +} diff --git a/server/src/entities/group_permissions.entity.ts b/server/src/entities/group_permissions.entity.ts index 0ff6e47b0b..dfc6256ebd 100644 --- a/server/src/entities/group_permissions.entity.ts +++ b/server/src/entities/group_permissions.entity.ts @@ -15,6 +15,7 @@ import { GranularPermissions } from './granular_permissions.entity'; import { GROUP_PERMISSIONS_TYPE } from '@modules/group-permissions/constants'; import { PageUser } from './page_users.entity'; import { QueryUser } from './query_users.entity'; +import { ComponentUser } from './component_users.entity'; @Entity({ name: 'permission_groups' }) export class GroupPermissions extends BaseEntity { @@ -70,5 +71,8 @@ export class GroupPermissions extends BaseEntity { @OneToMany(() => QueryUser, (queryUser) => queryUser.permissionGroup) queryUsers: QueryUser[]; + @OneToMany(() => ComponentUser, (componentUser) => componentUser.permissionGroup) + componentUsers: ComponentUser[]; + disabled?: boolean; } diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index 515b1f469e..f43396f0ed 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -31,6 +31,7 @@ import { AiResponseVote } from './ai_response_vote.entity'; import { USER_ROLE } from '@modules/group-permissions/constants'; import { PageUser } from './page_users.entity'; import { QueryUser } from './query_users.entity'; +import { ComponentUser } from './component_users.entity'; @Entity({ name: 'users' }) export class User extends BaseEntity { @@ -192,6 +193,9 @@ export class User extends BaseEntity { @OneToMany(() => QueryUser, (queryUser) => queryUser.user) queryUsers: QueryUser[]; + @OneToMany(() => ComponentUser, (componentUser) => componentUser.user) + componentUsers: ComponentUser[]; + organizationId: string; invitedOrganizationId: string; organizationIds?: Array; From 3a7f4be4bb223d727bad916f821e12591b2750bf Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 29 May 2025 17:50:04 +0530 Subject: [PATCH 03/45] Created base for API endpoint and feature keys --- .../modules/app-permissions/ability/index.ts | 9 ++++ .../app-permissions/constants/features.ts | 4 ++ .../app-permissions/constants/index.ts | 4 ++ .../src/modules/app-permissions/controller.ts | 46 +++++++++++++++++++ .../app-permissions/interfaces/IController.ts | 20 ++++++++ .../modules/app-permissions/types/index.ts | 4 ++ 6 files changed, 87 insertions(+) diff --git a/server/src/modules/app-permissions/ability/index.ts b/server/src/modules/app-permissions/ability/index.ts index 5a03f417d5..81b25601b1 100644 --- a/server/src/modules/app-permissions/ability/index.ts +++ b/server/src/modules/app-permissions/ability/index.ts @@ -42,6 +42,10 @@ export class FeatureAbilityFactory extends AbilityFactory FEATURE_KEY.CREATE_QUERY_PERMISSIONS, FEATURE_KEY.UPDATE_QUERY_PERMISSIONS, FEATURE_KEY.DELETE_QUERY_PERMISSIONS, + FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS, + FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS, + FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS, + FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS, ], App ); @@ -64,6 +68,10 @@ export class FeatureAbilityFactory extends AbilityFactory FEATURE_KEY.CREATE_QUERY_PERMISSIONS, FEATURE_KEY.UPDATE_QUERY_PERMISSIONS, FEATURE_KEY.DELETE_QUERY_PERMISSIONS, + FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS, + FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS, + FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS, + FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS, ], App ); @@ -80,6 +88,7 @@ export class FeatureAbilityFactory extends AbilityFactory FEATURE_KEY.FETCH_USER_GROUPS, FEATURE_KEY.FETCH_PAGE_PERMISSIONS, FEATURE_KEY.FETCH_QUERY_PERMISSIONS, + FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS, ], App ); diff --git a/server/src/modules/app-permissions/constants/features.ts b/server/src/modules/app-permissions/constants/features.ts index 360b1cf4c9..cd992b88aa 100644 --- a/server/src/modules/app-permissions/constants/features.ts +++ b/server/src/modules/app-permissions/constants/features.ts @@ -14,5 +14,9 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: {}, [FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: {}, [FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: {}, + [FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS]: {}, + [FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS]: {}, + [FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS]: {}, + [FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS]: {}, }, }; diff --git a/server/src/modules/app-permissions/constants/index.ts b/server/src/modules/app-permissions/constants/index.ts index ff5063e948..14e7122e2f 100644 --- a/server/src/modules/app-permissions/constants/index.ts +++ b/server/src/modules/app-permissions/constants/index.ts @@ -21,4 +21,8 @@ export enum FEATURE_KEY { CREATE_QUERY_PERMISSIONS = 'create_query_permissions', UPDATE_QUERY_PERMISSIONS = 'update_query_permissions', DELETE_QUERY_PERMISSIONS = 'delete_query_permissions', + FETCH_COMPONENT_PERMISSIONS = 'fetch_component_permissions', + CREATE_COMPONENT_PERMISSIONS = 'create_component_permissions', + UPDATE_COMPONENT_PERMISSIONS = 'update_component_permissions', + DELETE_COMPONENT_PERMISSIONS = 'delete_component_permissions', } diff --git a/server/src/modules/app-permissions/controller.ts b/server/src/modules/app-permissions/controller.ts index d317e3115b..65bdaf017d 100644 --- a/server/src/modules/app-permissions/controller.ts +++ b/server/src/modules/app-permissions/controller.ts @@ -127,4 +127,50 @@ export class AppPermissionsController implements IAppPermissionsController { ): Promise { throw new NotFoundException(); } + + @InitFeature(FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS) + @Get(':appId/components/:componentId') + async fetchComponentPermissions( + @User() user, + @Param('appId') appId: string, + @Param('componentId') componentId: string, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS) + @Post(':appId/components/:componentId') + async createComponentPermissions( + @User() user, + @Param('appId') appId: string, + @Param('componentId') componentId: string, + @Body() body: CreatePermissionDto, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS) + @Put(':appId/components/:componentId') + async updateComponentPermissions( + @User() user, + @Param('appId') appId: string, + @Param('componentId') componentId: string, + @Body() body: CreatePermissionDto, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS) + @Delete(':appId/components/:componentId') + async deleteComponentPermissions( + @User() user, + @Param('appId') appId: string, + @Param('componentId') componentId: string, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } } diff --git a/server/src/modules/app-permissions/interfaces/IController.ts b/server/src/modules/app-permissions/interfaces/IController.ts index a7808d280d..e9b9cdc24e 100644 --- a/server/src/modules/app-permissions/interfaces/IController.ts +++ b/server/src/modules/app-permissions/interfaces/IController.ts @@ -46,4 +46,24 @@ export interface IAppPermissionsController { ): Promise; deleteQueryPermissions(user: User, appId: string, queryId: string, response: Response): Promise; + + fetchComponentPermissions(user: User, appId: string, componentId: string, response: Response): Promise; + + createComponentPermissions( + user: User, + appId: string, + componentId: string, + body: CreatePermissionDto, + response: Response + ): Promise; + + updateComponentPermissions( + user: User, + appId: string, + componentId: string, + body: CreatePermissionDto, + response: Response + ): Promise; + + deleteComponentPermissions(user: User, appId: string, componentId: string, response: Response): Promise; } diff --git a/server/src/modules/app-permissions/types/index.ts b/server/src/modules/app-permissions/types/index.ts index d377f5a08f..5e3acf64b4 100644 --- a/server/src/modules/app-permissions/types/index.ts +++ b/server/src/modules/app-permissions/types/index.ts @@ -13,6 +13,10 @@ interface Features { [FEATURE_KEY.CREATE_QUERY_PERMISSIONS]: FeatureConfig; [FEATURE_KEY.UPDATE_QUERY_PERMISSIONS]: FeatureConfig; [FEATURE_KEY.DELETE_QUERY_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.FETCH_COMPONENT_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.CREATE_COMPONENT_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.UPDATE_COMPONENT_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.DELETE_COMPONENT_PERMISSIONS]: FeatureConfig; } export interface FeaturesConfig { From 5423bbe687abbad3f3186beb01c8e74ddd0e68ec Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 02:27:53 +0530 Subject: [PATCH 04/45] Implemented business logic for Component Permissions API --- server/ee | 2 +- .../interfaces/IUtilService.ts | 4 + server/src/modules/app-permissions/module.ts | 17 +++- .../component-permissions.repository.ts | 58 +++++++++++++ .../component-users.repository.ts | 83 +++++++++++++++++++ .../modules/app-permissions/util.service.ts | 8 ++ 6 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 server/src/modules/app-permissions/repositories/component-permissions.repository.ts create mode 100644 server/src/modules/app-permissions/repositories/component-users.repository.ts diff --git a/server/ee b/server/ee index dd0462d3c3..5865a18fe8 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit dd0462d3c3acf4b8e6856209eee52bc3cc78a02e +Subproject commit 5865a18fe87e545b9b6a8f40f998117edc403c3f diff --git a/server/src/modules/app-permissions/interfaces/IUtilService.ts b/server/src/modules/app-permissions/interfaces/IUtilService.ts index dbd390982a..a38d845076 100644 --- a/server/src/modules/app-permissions/interfaces/IUtilService.ts +++ b/server/src/modules/app-permissions/interfaces/IUtilService.ts @@ -14,4 +14,8 @@ export interface IUtilService { createQueryPermission(queryId: string, body: CreatePermissionDto): Promise; updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise; + + createComponentPermission(componentId: string, body: CreatePermissionDto): Promise; + + updateComponentPermission(componentId: string, body: CreatePermissionDto): Promise; } diff --git a/server/src/modules/app-permissions/module.ts b/server/src/modules/app-permissions/module.ts index 5e3e3db107..b290bd5dbc 100644 --- a/server/src/modules/app-permissions/module.ts +++ b/server/src/modules/app-permissions/module.ts @@ -9,10 +9,14 @@ import { PageUsersRepository } from './repositories/page-users.repository'; import { PagePermissionsRepository } from './repositories/page-permissions.repository'; import { QueryUsersRepository } from './repositories/query-users.repository'; import { QueryPermissionsRepository } from './repositories/query-permissions.repository'; +import { ComponentUsersRepository } from './repositories/component-users.repository'; +import { ComponentPermissionsRepository } from './repositories/component-permissions.repository'; import { PageUser } from '@entities/page_users.entity'; import { PagePermission } from '@entities/page_permissions.entity'; import { QueryUser } from '@entities/query_users.entity'; import { QueryPermission } from '@entities/query_permissions.entity'; +import { ComponentUser } from '@entities/component_users.entity'; +import { ComponentPermission } from '@entities/component_permissions.entity'; export class AppPermissionsModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -24,7 +28,16 @@ export class AppPermissionsModule { return { module: AppPermissionsModule, imports: [ - TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission, QueryUser, QueryPermission]), + TypeOrmModule.forFeature([ + GroupPermissions, + User, + PageUser, + PagePermission, + QueryUser, + QueryPermission, + ComponentUser, + ComponentPermission, + ]), ], controllers: [AppPermissionsController], providers: [ @@ -35,6 +48,8 @@ export class AppPermissionsModule { PagePermissionsRepository, QueryUsersRepository, QueryPermissionsRepository, + ComponentUsersRepository, + ComponentPermissionsRepository, FeatureAbilityFactory, ], exports: [AppPermissionsUtilService, AppPermissionsService], diff --git a/server/src/modules/app-permissions/repositories/component-permissions.repository.ts b/server/src/modules/app-permissions/repositories/component-permissions.repository.ts new file mode 100644 index 0000000000..802dd12868 --- /dev/null +++ b/server/src/modules/app-permissions/repositories/component-permissions.repository.ts @@ -0,0 +1,58 @@ +import { ComponentPermission } from '@entities/component_permissions.entity'; +import { Injectable } from '@nestjs/common'; +import { DataSource, EntityManager, Repository } from 'typeorm'; +import { ComponentUsersRepository } from './component-users.repository'; +import { dbTransactionWrap } from '@helpers/database.helper'; +import { PAGE_PERMISSION_TYPE } from '../constants'; + +@Injectable() +export class ComponentPermissionsRepository extends Repository { + constructor(private dataSource: DataSource, private readonly componentUsersRepository: ComponentUsersRepository) { + super(ComponentPermission, dataSource.createEntityManager()); + } + + async getComponentPermissions(componentId: string, manager?: EntityManager): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const componentPermissions = await manager.find(ComponentPermission, { + where: { componentId }, + relations: ['users', 'users.user', 'users.permissionGroup'], + }); + + return componentPermissions.map((permission) => { + if (permission.type === PAGE_PERMISSION_TYPE.GROUP) { + return { + ...permission, + groups: permission.users, + users: undefined, + }; + } + return permission; + }); + }, manager || this.manager); + } + + async createComponentPermissions( + componentId: string, + type: PAGE_PERMISSION_TYPE, + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const existingPermission = await manager.findOne(ComponentPermission, { where: { componentId } }); + if (existingPermission) { + throw new Error(`Component permission already exists for Component id: ${componentId}`); + } + + const componentPermission = manager.create(ComponentPermission, { + componentId, + type, + }); + return manager.save(componentPermission); + }, manager || this.manager); + } + + async deleteComponentPermissions(componentId: string, manager?: EntityManager): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + await manager.delete(ComponentPermission, { componentId }); + }, manager || this.manager); + } +} diff --git a/server/src/modules/app-permissions/repositories/component-users.repository.ts b/server/src/modules/app-permissions/repositories/component-users.repository.ts new file mode 100644 index 0000000000..98478b4334 --- /dev/null +++ b/server/src/modules/app-permissions/repositories/component-users.repository.ts @@ -0,0 +1,83 @@ +import { ComponentUser } from '@entities/component_users.entity'; +import { Injectable } from '@nestjs/common'; +import { DataSource, EntityManager, Repository } from 'typeorm'; +import { dbTransactionWrap } from '@helpers/database.helper'; +import { ComponentPermission } from '@entities/component_permissions.entity'; + +@Injectable() +export class ComponentUsersRepository extends Repository { + constructor(private dataSource: DataSource) { + super(ComponentUser, dataSource.createEntityManager()); + } + + async createComponentUsersWithSingle( + componentPermissionsId: string, + users: string[], + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const componentUsers = users.map((userId) => { + return manager.create(ComponentUser, { + componentPermissionsId, + userId, + permissionGroupsId: null, + }); + }); + return manager.save(componentUsers); + }, manager || this.manager); + } + + async createComponentUsersWithGroup( + componentPermissionsId: string, + groups: string[], + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const componentUsers = groups.map((permissionGroupsId) => { + return manager.create(ComponentUser, { + componentPermissionsId, + permissionGroupsId, + userId: null, + }); + }); + return manager.save(componentUsers); + }, manager || this.manager); + } + + async checkComponentUserWithGroup( + componentPermission: ComponentPermission, + userId: string, + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const result = await manager + .createQueryBuilder(ComponentUser, 'component_users') + .innerJoin('component_users.permissionGroup', 'group') + .innerJoin('group.groupUsers', 'groupUser') + .where('component_users.componentPermission = :permissionId', { + permissionId: componentPermission.id, + }) + .andWhere('groupUser.userId = :userId', { userId }) + .getOne(); + + return !!result; + }, manager || this.manager); + } + + async checkComponentUserWithSingle( + componentPermission: ComponentPermission, + userId: string, + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const componentUser = await manager.findOne(ComponentUser, { + where: { + componentPermission: { id: componentPermission.id }, + userId, + }, + }); + + return !!componentUser; + }, manager || this.manager); + } +} diff --git a/server/src/modules/app-permissions/util.service.ts b/server/src/modules/app-permissions/util.service.ts index 7ff894a058..55d927eebb 100644 --- a/server/src/modules/app-permissions/util.service.ts +++ b/server/src/modules/app-permissions/util.service.ts @@ -31,4 +31,12 @@ export class AppPermissionsUtilService implements IUtilService { async updateQueryPermission(queryId: string, body: CreatePermissionDto): Promise { throw new Error('Method not implemented.'); } + + async createComponentPermission(componentId: string, body: CreatePermissionDto): Promise { + throw new Error('Method not implemented.'); + } + + async updateComponentPermission(componentId: string, body: CreatePermissionDto): Promise { + throw new Error('Method not implemented.'); + } } From 827c1ba7697b42d783b595b3d75e2619336bba76 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 11:39:56 +0530 Subject: [PATCH 05/45] Added filter logic for preview and released apps --- server/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ee b/server/ee index 5865a18fe8..78d8d151ae 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 5865a18fe87e545b9b6a8f40f998117edc403c3f +Subproject commit 78d8d151ae6be18fa76bf46fdf25ffa7dec7a643 From cf30360b3db083b71906cd638559ad7579274de8 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 14:13:29 +0530 Subject: [PATCH 06/45] Implemented versioning logic for component permissions --- server/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ee b/server/ee index 78d8d151ae..eabc2cc32a 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 78d8d151ae6be18fa76bf46fdf25ffa7dec7a643 +Subproject commit eabc2cc32afac238c3f7c01198bf049af4cf3b60 From bc0d4b79d3d43073a207de53864d38bc6c2af68d Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 15:38:55 +0530 Subject: [PATCH 07/45] Implemented import-export logic --- .../services/app-import-export.service.ts | 75 ++++++++++++++++++- .../import-export-resources/service.ts | 10 ++- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/server/src/modules/apps/services/app-import-export.service.ts b/server/src/modules/apps/services/app-import-export.service.ts index 6522690675..9b4c1be177 100644 --- a/server/src/modules/apps/services/app-import-export.service.ts +++ b/server/src/modules/apps/services/app-import-export.service.ts @@ -41,6 +41,8 @@ import { PageUser } from '@entities/page_users.entity'; import { UsersUtilService } from '@modules/users/util.service'; import { QueryPermission } from '@entities/query_permissions.entity'; import { QueryUser } from '@entities/query_users.entity'; +import { ComponentPermission } from '@entities/component_permissions.entity'; +import { ComponentUser } from '@entities/component_users.entity'; interface AppResourceMappings { defaultDataSourceIdMapping: Record; dataQueryMapping: Record; @@ -241,6 +243,9 @@ export class AppImportExportService { ? await manager .createQueryBuilder(Component, 'components') .leftJoinAndSelect('components.layouts', 'layouts') + .leftJoinAndSelect('components.permissions', 'permission') + .leftJoinAndSelect('permission.users', 'componentUser') + .leftJoinAndSelect('componentUser.permissionGroup', 'permissionGroup') .where('components.pageId IN(:...pageId)', { pageId: pages.map((v) => v.id), }) @@ -248,6 +253,21 @@ export class AppImportExportService { .getMany() : []; + const componentsWithPermissionGroups = components.map((component) => { + const groupPermission = component.permissions.find((perm) => perm.type === 'GROUP'); + + return { + ...component, + permissions: groupPermission + ? { + permissionGroup: groupPermission.users + .map((user) => user.permissionGroup?.name) + .filter((name): name is string => Boolean(name)), + } + : undefined, + }; + }); + const events = await manager .createQueryBuilder(EventHandler, 'event_handlers') .where('event_handlers.appVersionId IN(:...versionId)', { @@ -256,7 +276,7 @@ export class AppImportExportService { .orderBy('event_handlers.created_at', 'ASC') .getMany(); - appToExport['components'] = components; + appToExport['components'] = componentsWithPermissionGroups; appToExport['pages'] = pagesWithPermissionGroups; appToExport['events'] = events; appToExport['dataQueries'] = queriesWithPermissionGroups; @@ -950,6 +970,13 @@ export class AppImportExportService { await manager.save(newLayout); }); + if (component.permissions) { + savedComponent.permissions = component.permissions; + } + + //create component permissions of component if flag enabled in dto + await this.createComponentPermissionsForGroups(savedComponent, user.organizationId, manager); + const componentEvents = importingEvents.filter((event) => event.sourceId === component.id); if (componentEvents.length > 0) { @@ -1386,7 +1413,7 @@ export class AppImportExportService { return pageSettings; } - async checkIfGroupPermissionsExist(pages, queries, organizationId) { + async checkIfGroupPermissionsExist(pages, queries, components, organizationId) { const allGroupNames = new Set(); for (const page of pages) { @@ -1405,6 +1432,15 @@ export class AppImportExportService { } } + for (const component of components) { + const groupNames = component.permissions?.permissionGroup || []; + for (const name of groupNames) { + if (!allGroupNames.has(name)) { + allGroupNames.add(name); + } + } + } + if (!allGroupNames.size) return; return await dbTransactionWrap(async (manager: EntityManager) => { @@ -1500,6 +1536,41 @@ export class AppImportExportService { await manager.save(queryUsers); } + async createComponentPermissionsForGroups(component, organizationId: string, manager: EntityManager) { + const groupNames = component.permissions?.permissionGroup || []; + if (!groupNames.length) return; + + const existingGroups = await manager + .createQueryBuilder(GroupPermissions, 'gp') + .where('gp.name IN (:...names)', { names: groupNames }) + .andWhere('gp.organizationId = :organizationId', { organizationId }) + .getMany(); + + const groupMap = new Map(existingGroups.map((g) => [g.name, g])); + + // Filter to only existing group names + const validGroupNames = groupNames.filter((name) => groupMap.has(name)); + + // If no valid group names exist, do not create permissions + if (!validGroupNames.length) return; + + const permission = manager.create(ComponentPermission, { + componentId: component.id, + type: PAGE_PERMISSION_TYPE.GROUP, + }); + + const savedPermission = await manager.save(permission); + + const componentUsers = validGroupNames.map((name) => + manager.create(ComponentUser, { + componentPermissionsId: savedPermission.id, + permissionGroupsId: groupMap.get(name).id, + }) + ); + + await manager.save(componentUsers); + } + async createAppVersionsForImportedApp( manager: EntityManager, user: User, diff --git a/server/src/modules/import-export-resources/service.ts b/server/src/modules/import-export-resources/service.ts index 7b50ca6076..a83b3f766e 100644 --- a/server/src/modules/import-export-resources/service.ts +++ b/server/src/modules/import-export-resources/service.ts @@ -78,8 +78,14 @@ export class ImportExportResourcesService { appParams = { ...appParams.appV2 }; const pages = appParams?.pages; const queries = appParams?.dataQueries; - (pages?.length || queries?.length) && - (await this.appImportExportService.checkIfGroupPermissionsExist(pages, queries, user.organizationId)); + const components = appParams?.components; + (pages?.length || queries?.length || components?.length) && + (await this.appImportExportService.checkIfGroupPermissionsExist( + pages, + queries, + components, + user.organizationId + )); } } } From a79ffd58040987aed363a856d1f032fe0e84c4c7 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 15:42:04 +0530 Subject: [PATCH 08/45] Create service functions for component permissions --- .../src/_services/appPermission.service.js | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/frontend/src/_services/appPermission.service.js b/frontend/src/_services/appPermission.service.js index fd2438f058..1aab6e97e1 100644 --- a/frontend/src/_services/appPermission.service.js +++ b/frontend/src/_services/appPermission.service.js @@ -11,6 +11,10 @@ export const appPermissionService = { createQueryPermission, updateQueryPermission, deleteQueryPermission, + getComponentPermission, + createComponentPermission, + updateComponentPermission, + deleteComponentPermission, }; function getPagePermission(appId, pageId) { @@ -89,3 +93,49 @@ function deleteQueryPermission(appId, queryId) { }; return fetch(`${config.apiUrl}/app-permissions/${appId}/queries/${queryId}`, requestOptions).then(handleResponse); } + +function getComponentPermission(appId, componentId) { + const requestOptions = { + method: 'GET', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then( + handleResponse + ); +} + +function createComponentPermission(appId, componentId, body) { + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then( + handleResponse + ); +} + +function updateComponentPermission(appId, componentId, body) { + const requestOptions = { + method: 'PUT', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then( + handleResponse + ); +} + +function deleteComponentPermission(appId, componentId) { + const requestOptions = { + method: 'DELETE', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/app-permissions/${appId}/components/${componentId}`, requestOptions).then( + handleResponse + ); +} From 2261e96f2e27449c668914cd700c39f616facf08 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 30 May 2025 18:18:00 +0530 Subject: [PATCH 09/45] Get permissions data with components in Editor --- server/ee | 2 +- server/src/modules/apps/interfaces/services/IPageService.ts | 2 +- server/src/modules/apps/services/page.service.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/ee b/server/ee index eabc2cc32a..adc0f3471b 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit eabc2cc32afac238c3f7c01198bf049af4cf3b60 +Subproject commit adc0f3471bfef26be3ea5d69972bcf2e6c8c46cb diff --git a/server/src/modules/apps/interfaces/services/IPageService.ts b/server/src/modules/apps/interfaces/services/IPageService.ts index f8f8fed9eb..9f06d02719 100644 --- a/server/src/modules/apps/interfaces/services/IPageService.ts +++ b/server/src/modules/apps/interfaces/services/IPageService.ts @@ -4,7 +4,7 @@ import { EventHandler } from 'src/entities/event_handler.entity'; import { CreatePageDto, UpdatePageDto } from '@modules/apps/dto/page'; export interface IPageService { - findPagesForVersion(appVersionId: string): Promise; + findPagesForVersion(appVersionId: string, mode?: string): Promise; findOne(id: string): Promise; createPage(page: CreatePageDto, appVersionId: string): Promise; clonePage(pageId: string, appVersionId: string): Promise<{ pages: Page[]; events: EventHandler[] }>; diff --git a/server/src/modules/apps/services/page.service.ts b/server/src/modules/apps/services/page.service.ts index fa0b2864e0..3ead2d4a99 100644 --- a/server/src/modules/apps/services/page.service.ts +++ b/server/src/modules/apps/services/page.service.ts @@ -23,7 +23,7 @@ export class PageService implements IPageService { protected eventHandlerService: EventsService ) {} - async findPagesForVersion(appVersionId: string): Promise { + async findPagesForVersion(appVersionId: string, mode?: string): Promise { // const allPages = await this.pageRepository.find({ where: { appVersionId }, order: { index: 'ASC' } }); const allPages = await this.pageHelperService.fetchPages(appVersionId); const pagesWithComponents = await Promise.all( From b286834c7f47c7e8086e0c8f06d7286eff41a1a8 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Sun, 1 Jun 2025 20:12:22 +0530 Subject: [PATCH 10/45] Integrated App permissions modal for component permissions and updated state for permissions --- frontend/ee | 2 +- .../RightSideBar/Inspector/Inspector.jsx | 86 +++++++++++++++---- .../_stores/slices/componentsSlice.js | 22 +++++ server/ee | 2 +- 4 files changed, 93 insertions(+), 19 deletions(-) diff --git a/frontend/ee b/frontend/ee index 5c2787b498..d97f9b8c1c 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 5c2787b498202f4356b4598fb961ddbab04acbbf +Subproject commit d97f9b8c1c0dd54696d3c13fc9dc2227db6c9664 diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx index 07b53c4454..b661b74583 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx @@ -43,6 +43,9 @@ import useStore from '@/AppBuilder/_stores/store'; import { componentTypes } from '@/AppBuilder/WidgetManager/componentTypes'; import { copyComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils.js'; import DatetimePickerV2 from './Components/DatetimePickerV2.jsx'; +import { ToolTip } from '@/_components/ToolTip'; +import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal'; +import { appPermissionService } from '@/_services'; const INSPECTOR_HEADER_OPTIONS = [ { @@ -60,6 +63,19 @@ const INSPECTOR_HEADER_OPTIONS = [ value: 'duplicate', icon: , }, + { + label: 'Component permission', + value: 'permission', + icon: ( + permission-icon + ), + trailingIcon: , + }, { label: 'Delete', value: 'delete', @@ -103,6 +119,11 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte const isVersionReleased = useStore((state) => state.isVersionReleased); const setWidgetDeleteConfirmation = useStore((state) => state.setWidgetDeleteConfirmation); const setComponentToInspect = useStore((state) => state.setComponentToInspect); + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; + const showComponentPermissionModal = useStore((state) => state.showComponentPermissionModal); + const toggleComponentPermissionModal = useStore((state) => state.toggleComponentPermissionModal); + const setComponentPermission = useStore((state) => state.setComponentPermission); const dataQueries = useDataQueries(); const currentState = useCurrentState(); @@ -375,6 +396,9 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte if (value === 'delete') { setWidgetDeleteConfirmation(true); } + if (value === 'permission' && licenseValid) { + toggleComponentPermissionModal(true); + } if (value === 'duplicate') { copyComponents({ isCloning: true }); } @@ -490,26 +514,42 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte overlay={ - {INSPECTOR_HEADER_OPTIONS.map((option) => ( -
{ - e.stopPropagation(); - handleInspectorHeaderActions(option.value); - }} - > -
{option.icon}
+ {INSPECTOR_HEADER_OPTIONS.map((option) => { + const optionBody = (
{ + e.stopPropagation(); + handleInspectorHeaderActions(option.value); + }} > - {option?.label} +
{option.icon}
+
+ {option?.label} +
+ {option.value === 'permission' && !licenseValid && option.trailingIcon && option.trailingIcon}
-
- ))} + ); + + return option.value === 'permission' ? ( + + {optionBody} + + ) : ( + optionBody + ); + })}
} @@ -519,6 +559,18 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte + appPermissionService.getComponentPermission(appId, id)} + createPermission={(id, appId, body) => appPermissionService.createComponentPermission(appId, id, body)} + updatePermission={(id, appId, body) => appPermissionService.updateComponentPermission(appId, id, body)} + deletePermission={(id, appId) => appPermissionService.deleteComponentPermission(appId, id)} + onSuccess={(data) => setComponentPermission(selectedComponentId, data)} + />
diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index b500a4d912..46ae6791e3 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -45,6 +45,7 @@ const initialState = { showWidgetDeleteConfirmation: false, focusedParentId: null, modalsOpenOnCanvas: [], + showComponentPermissionModal: false, }; export const createComponentsSlice = (set, get) => ({ @@ -1920,4 +1921,25 @@ export const createComponentsSlice = (set, get) => ({ setComponentProperty(componentId, `canvasHeight`, maxHeight, 'properties', 'value', false); }, + toggleComponentPermissionModal: (show) => { + set((state) => { + state.showComponentPermissionModal = show; + }); + }, + setComponentPermission: (componentId, data) => { + const { currentPageId, modules } = get(); + const currentPageIndex = modules.canvas.pages.findIndex((page) => page.id === currentPageId); + const component = modules.canvas.pages[currentPageIndex]?.components?.[componentId]; + + if (component) { + const updatedComponent = { + ...component, + permissions: data.length === 0 || data.length === undefined ? [] : [data[0]], + }; + + set((state) => { + state.modules.canvas.pages[currentPageIndex].components[componentId] = updatedComponent; + }); + } + }, }); diff --git a/server/ee b/server/ee index adc0f3471b..d32bbd96c6 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit adc0f3471bfef26be3ea5d69972bcf2e6c8c46cb +Subproject commit d32bbd96c6d81f7cfb4237fd92321448fc6f2e34 From 19b770abad3b18e80c8a5f750a6ceeae411b5641 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Sun, 1 Jun 2025 20:30:30 +0530 Subject: [PATCH 11/45] Remove component from containerChildrenMaping --- frontend/src/AppBuilder/_stores/slices/componentsSlice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index 46ae6791e3..8de2e30723 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -80,6 +80,7 @@ export const createComponentsSlice = (set, get) => ({ state.currentPageId = id; state.containerChildrenMapping = { canvas: [] }; Object.entries(currentPageComponents).forEach(([componentId, component]) => { + if (state.currentMode === 'view' && component.restricted) return; const parentId = component.component.parent || 'canvas'; if (!state.containerChildrenMapping[parentId]) { state.containerChildrenMapping[parentId] = []; From 15719ac1f288208c7f62fb4960e09abfac00f971 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Sun, 1 Jun 2025 22:27:32 +0530 Subject: [PATCH 12/45] Add permission icon in component config handle --- .../AppCanvas/ConfigHandle/ConfigHandle.jsx | 49 +++++++++++++++++++ frontend/src/_styles/theme.scss | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx index 45835c39de..cdf864d8b5 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx @@ -4,6 +4,7 @@ import './configHandle.scss'; import useStore from '@/AppBuilder/_stores/store'; import { findHighestLevelofSelection } from '../Grid/gridUtils'; import SolidIcon from '@/_ui/Icon/solidIcons/index'; +import { ToolTip } from '@/_components/ToolTip'; const CONFIG_HANDLE_HEIGHT = 20; const BUFFER_HEIGHT = 1; @@ -47,7 +48,39 @@ export const ConfigHandle = ({ ); }, shallow); + const currentPageIndex = useStore((state) => state.currentPageIndex); + const component = useStore((state) => state.modules.canvas.pages[currentPageIndex].components[id]); + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; + const isRestricted = component.permissions && component.permissions.length !== 0; + let height = visibility === false ? 10 : widgetHeight; + + const getTooltip = () => { + const permission = component.permissions?.[0]; + if (!permission) return null; + + const users = permission.groups || permission.users || []; + if (users.length === 0) return null; + + const isSingle = permission.type === 'SINGLE'; + const isGroup = permission.type === 'GROUP'; + + if (isSingle) { + return users.length === 1 + ? `Access restricted to ${users[0].user.email}` + : `Access restricted to ${users.length} users`; + } + + if (isGroup) { + return users.length === 1 + ? `Access restricted to ${users[0].permission_group?.name || users[0].permissionGroup?.name} group` + : `Access restricted to ${users.length} user groups`; + } + + return null; + }; + return (
+ {licenseValid && isRestricted && ( + + + + + + )} Date: Tue, 3 Jun 2025 22:24:47 +0530 Subject: [PATCH 13/45] Restrict child component for container components --- .../src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx | 3 ++- frontend/src/AppBuilder/_stores/slices/componentsSlice.js | 1 - server/ee | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx index cdf864d8b5..8e79d6f63c 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx @@ -53,6 +53,7 @@ export const ConfigHandle = ({ const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; const isRestricted = component.permissions && component.permissions.length !== 0; + const draggingComponentId = useStore((state) => state.draggingComponentId); let height = visibility === false ? 10 : widgetHeight; @@ -105,7 +106,7 @@ export const ConfigHandle = ({ }} > {licenseValid && isRestricted && ( - + ({ state.currentPageId = id; state.containerChildrenMapping = { canvas: [] }; Object.entries(currentPageComponents).forEach(([componentId, component]) => { - if (state.currentMode === 'view' && component.restricted) return; const parentId = component.component.parent || 'canvas'; if (!state.containerChildrenMapping[parentId]) { state.containerChildrenMapping[parentId] = []; diff --git a/server/ee b/server/ee index d32bbd96c6..0047b9e0c6 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit d32bbd96c6d81f7cfb4237fd92321448fc6f2e34 +Subproject commit 0047b9e0c6d1ea6bed11de4333ac6961ec030eab From a3450b87f7d629437d9ddc51c40b88e25b04d782 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Tue, 3 Jun 2025 23:44:26 +0530 Subject: [PATCH 14/45] Disable button on free plan --- frontend/src/AppBuilder/QueryPanel/QueryCard.jsx | 1 + .../src/AppBuilder/RightSideBar/Inspector/Inspector.jsx | 7 +++++-- frontend/src/ToolJetUI/List/list.scss | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx index 5236a529eb..318ac86736 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx @@ -276,6 +276,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
{option?.label} diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx index b661b74583..c6c788ecaf 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx @@ -396,12 +396,14 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte if (value === 'delete') { setWidgetDeleteConfirmation(true); } - if (value === 'permission' && licenseValid) { + if (value === 'permission') { + if (!licenseValid) return; toggleComponentPermissionModal(true); } if (value === 'duplicate') { copyComponents({ isCloning: true }); } + setShowHeaderActionsMenu(false); }; const buildGeneralStyle = () => { if (!componentMeta?.definition?.generalStyles) { @@ -467,7 +469,7 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte React.useEffect(() => { const handleClickOutside = (event) => { - if (showHeaderActionsMenu && event.target.closest('.list-menu') === null) { + if (showHeaderActionsMenu && event.target.closest('#list-menu') === null) { setShowHeaderActionsMenu(false); } }; @@ -529,6 +531,7 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
{option?.label} diff --git a/frontend/src/ToolJetUI/List/list.scss b/frontend/src/ToolJetUI/List/list.scss index 786f4e88af..04258a0788 100644 --- a/frontend/src/ToolJetUI/List/list.scss +++ b/frontend/src/ToolJetUI/List/list.scss @@ -79,6 +79,10 @@ button:focus:not(:focus-visible) { .color-tomato9 { color: var(--tomato9) } + + .color-disabled { + color: var(--text-disabled); + } } } From 62b44532417662581a6473597224f5602b579109 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 5 Jun 2025 00:44:01 +0530 Subject: [PATCH 15/45] Fix: Query options menu --- .../src/AppBuilder/QueryPanel/QueryCard.jsx | 192 ++++-------------- .../AppBuilder/QueryPanel/QueryCardMenu.jsx | 172 ++++++++++++++++ .../AppBuilder/QueryPanel/QueryDataPane.jsx | 2 + .../_stores/slices/queryPanelSlice.js | 19 ++ 4 files changed, 231 insertions(+), 154 deletions(-) create mode 100644 frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx index 318ac86736..2d192ec666 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx @@ -1,11 +1,10 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState } from 'react'; import { Tooltip } from 'react-tooltip'; import { ToolTip } from '@/_components/ToolTip'; import { updateQuerySuggestions } from '@/_helpers/appUtils'; // import { Confirm } from '../Viewer/Confirm'; import { toast } from 'react-hot-toast'; import { shallow } from 'zustand/shallow'; -import Copy from '@/_ui/Icon/solidIcons/Copy'; import DataSourceIcon from '../QueryManager/Components/DataSourceIcon'; import { isQueryRunnable, decodeEntities } from '@/_helpers/utils'; import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers'; @@ -13,17 +12,10 @@ import useStore from '@/AppBuilder/_stores/store'; //TODO: Remove this import { Confirm } from '@/Editor/Viewer/Confirm'; // TODO: enable delete query confirmation popup -import { debounce } from 'lodash'; import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx'; -import Edit from '@/_ui/Icon/bulkIcons/Edit'; -import Trash from '@/_ui/Icon/solidIcons/Trash'; -import { OverlayTrigger, Popover } from 'react-bootstrap'; -import classNames from 'classnames'; import SolidIcon from '@/_ui/Icon/SolidIcons'; export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => { - const appId = useStore((state) => state.app.appId); - const isQuerySelected = useStore((state) => state.queryPanel.isQuerySelected(dataQuery.id), shallow); const setSelectedQuery = useStore((state) => state.queryPanel.setSelectedQuery); const checkExistingQueryName = useStore((state) => state.dataQuery.checkExistingQueryName); @@ -31,11 +23,16 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => { const isDeletingQueryInProcess = useStore((state) => state.dataQuery.isDeletingQueryInProcess); const renameQuery = useStore((state) => state.dataQuery.renameQuery); const deleteDataQueries = useStore((state) => state.dataQuery.deleteDataQueries); - const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery); const setPreviewData = useStore((state) => state.queryPanel.setPreviewData); - const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal); - const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); - const [showQueryMenu, setShowQueryMenu] = useState(false); + const shouldFreeze = useStore((state) => state.getShouldFreeze()); + + const renamingQueryId = useStore((state) => state.queryPanel.renamingQueryId); + const deletingQueryId = useStore((state) => state.queryPanel.deletingQueryId); + const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery); + const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery); + const isRenaming = renamingQueryId === dataQuery.id; + const isDeleting = deletingQueryId === dataQuery.id; + const hasPermissions = selectedDataSourceScope === 'global' ? canUpdateDataSource(dataQuery?.data_source_id) || @@ -43,104 +40,35 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => { canDeleteDataSource() : true; + const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu); const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; const isRestricted = dataQuery.permissions && dataQuery.permissions.length !== 0; - const shouldFreeze = useStore((state) => state.getShouldFreeze()); - - const QUERY_MENU_OPTIONS = [ - { - label: 'Rename', - value: 'rename', - icon: , - showTooltip: false, - }, - { - label: 'Duplicate', - value: 'duplicate', - icon: , - showTooltip: false, - }, - { - label: 'Query permission', - value: 'permission', - icon: ( - permission-icon - ), - trailingIcon: !licenseValid ? : undefined, - tooltipText: 'Query permissions are available only in paid plans', - showTooltip: !licenseValid, - }, - { - label: 'Delete', - value: 'delete', - icon: , - showTooltip: false, - }, - ]; - - const handleQueryMenuActions = (value) => { - if (value === 'rename') { - setRenamingQuery(true); - } - if (value === 'duplicate') { - debouncedDuplicateQuery(dataQuery?.id, appId); - } - if (value === 'permission') { - if (!licenseValid) return; - toggleQueryPermissionModal(true); - } - if (value === 'delete') { - deleteDataQuery(); - } - setShowQueryMenu(false); - }; - - const [renamingQuery, setRenamingQuery] = useState(false); - - const deleteDataQuery = () => { - setShowDeleteConfirmation(true); - }; - const updateQueryName = (dataQuery, newName) => { const { name } = dataQuery; if (name === newName) { - return setRenamingQuery(false); + return setRenamingQuery(null); } const isNewQueryNameAlreadyExists = checkExistingQueryName(newName); if (newName && !isNewQueryNameAlreadyExists) { renameQuery(dataQuery?.id, newName); - setRenamingQuery(false); + setRenamingQuery(null); updateQuerySuggestions(name, newName); } else { if (isNewQueryNameAlreadyExists) { toast.error('Query name already exists'); } - setRenamingQuery(false); + setRenamingQuery(null); } }; const executeDataQueryDeletion = () => { - setShowDeleteConfirmation(false); + deleteDataQuery(null); deleteDataQueries(dataQuery?.id); setPreviewData(null); }; - // To prevent user clicking from continuous clicks - const debouncedDuplicateQuery = useCallback( - debounce((queryId, appId) => { - duplicateQuery(queryId, appId); - setPreviewData(null); - }, 500), - [duplicateQuery] - ); - const getTooltip = () => { const permission = dataQuery.permissions?.[0]; if (!permission) return null; @@ -171,10 +99,18 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
{ + onClick={(e) => { if (isQuerySelected) return; - setSelectedQuery(dataQuery?.id); - setPreviewData(null); + const menuBtn = document.getElementById(`query-handler-menu-${dataQuery?.id}`); + if (menuBtn.contains(e.target)) { + e.stopPropagation(); + } else { + toggleQueryHandlerMenu(false); + } + setTimeout(() => { + setSelectedQuery(dataQuery?.id); + setPreviewData(null); + }, 0); }} role="button" > @@ -182,7 +118,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
- {renamingQuery ? ( + {isRenaming ? ( { )}
- setShowQueryMenu(false)} - show={showQueryMenu && isQuerySelected} - popperConfig={{ - modifiers: [ - { - name: 'offset', - options: { - offset: [0, 3], - }, - }, - ], - }} - overlay={ - - - {QUERY_MENU_OPTIONS.map((option) => ( - -
{ - e.stopPropagation(); - handleQueryMenuActions(option.value); - }} - > -
{option.icon}
-
- {option?.label} -
- {option.trailingIcon && option.trailingIcon} -
-
- ))} -
-
- } - > - setShowQueryMenu(!showQueryMenu)} - size="small" - variant="outline" - className="" - /> -
+ toggleQueryHandlerMenu(true, `query-handler-menu-${dataQuery?.id}`)} + size="small" + variant="outline" + className="" + id={`query-handler-menu-${dataQuery?.id}`} + />
setShowDeleteConfirmation(false)} + onCancel={() => deleteDataQuery(null)} darkMode={darkMode} /> diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx new file mode 100644 index 0000000000..760adb8075 --- /dev/null +++ b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx @@ -0,0 +1,172 @@ +import React, { useCallback } from 'react'; +import { Overlay, Popover } from 'react-bootstrap'; +import useStore from '@/AppBuilder/_stores/store'; +import classNames from 'classnames'; +import Edit from '@/_ui/Icon/bulkIcons/Edit'; +import Trash from '@/_ui/Icon/solidIcons/Trash'; +import Copy from '@/_ui/Icon/solidIcons/Copy'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { shallow } from 'zustand/shallow'; +import { ToolTip } from '@/_components/ToolTip'; +import { debounce } from 'lodash'; +import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver'; + +const QueryCardMenu = ({ darkMode }) => { + const appId = useStore((state) => state.app.appId); + const selectedQuery = useStore((state) => state.queryPanel.selectedQuery); + const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal); + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; + const targetBtnForMenu = useStore((state) => state.queryPanel.targetBtnForMenu); + const targetElement = document.getElementById(targetBtnForMenu); + const showQueryHandlerMenu = useStore((state) => state.queryPanel.showQueryHandlerMenu); + const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu); + const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery); + const setPreviewData = useStore((state) => state.queryPanel.setPreviewData); + const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery); + const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery); + + const QUERY_MENU_OPTIONS = [ + { + label: 'Rename', + value: 'rename', + icon: , + showTooltip: false, + }, + { + label: 'Duplicate', + value: 'duplicate', + icon: , + showTooltip: false, + }, + { + label: 'Query permission', + value: 'permission', + icon: ( + permission-icon + ), + trailingIcon: , + }, + { + label: 'Delete', + value: 'delete', + icon: , + showTooltip: false, + }, + ]; + + // To prevent user clicking from continuous clicks + const debouncedDuplicateQuery = useCallback( + debounce((queryId, appId) => { + duplicateQuery(queryId, appId); + setPreviewData(null); + }, 500), + [duplicateQuery] + ); + + const handleQueryMenuActions = (value) => { + if (value === 'rename') { + setRenamingQuery(selectedQuery?.id); + } + if (value === 'duplicate') { + debouncedDuplicateQuery(selectedQuery?.id, appId); + } + if (value === 'permission') { + if (!licenseValid) return; + toggleQueryPermissionModal(true); + } + if (value === 'delete') { + deleteDataQuery(selectedQuery?.id); + } + toggleQueryHandlerMenu(false); + }; + + usePopoverObserver( + document.getElementsByClassName('query-list')[0], + targetElement, + document.getElementById('query-list-menu'), + showQueryHandlerMenu, + () => (document.getElementById('query-list-menu').style.display = 'block'), + () => (document.getElementById('query-list-menu').style.display = 'none') + ); + + return ( + toggleQueryHandlerMenu(false)} + popperConfig={{ + modifiers: [ + { + name: 'flip', + options: { + fallbackPlacements: ['top-start'], + flipVariations: true, + allowedAutoPlacements: ['top', 'bottom'], + boundary: 'viewport', + }, + }, + { + name: 'offset', + options: { + offset: [0, 3], + }, + }, + ], + }} + > + {(props) => ( + + + {QUERY_MENU_OPTIONS.map((option) => { + const optionBody = ( +
{ + e.stopPropagation(); + handleQueryMenuActions(option.value); + }} + > +
{option.icon}
+
+ {option?.label} +
+ {option.value === 'permission' && !licenseValid && option.trailingIcon && option.trailingIcon} +
+ ); + + return option.value === 'permission' ? ( + + {optionBody} + + ) : ( + optionBody + ); + })} +
+
+ )} +
+ ); +}; + +export default QueryCardMenu; diff --git a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx index 62f27197ba..12e8fb3171 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx @@ -19,6 +19,7 @@ import useStore from '@/AppBuilder/_stores/store'; import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal'; import { shallow } from 'zustand/shallow'; import { appPermissionService } from '@/_services'; +import QueryCardMenu from './QueryCardMenu'; export const QueryDataPane = ({ darkMode }) => { const { t } = useTranslation(); @@ -180,6 +181,7 @@ export const QueryDataPane = ({ darkMode }) => { {filteredQueries.map((query) => ( ))} + {licenseValid && ( ({ @@ -1141,5 +1146,19 @@ export const createQueryPanelSlice = (set, get) => ({ state.queryPanel.showQueryPermissionModal = show; }); }, + toggleQueryHandlerMenu: (show, id) => { + set((state) => { + if (show) state.queryPanel.targetBtnForMenu = id; + state.queryPanel.showQueryHandlerMenu = show; + }); + }, + setRenamingQuery: (queryId) => + set((state) => { + state.queryPanel.renamingQueryId = queryId; + }), + deleteDataQuery: (queryId) => + set((state) => { + state.queryPanel.deletingQueryId = queryId; + }), }, }); From 0dc73d891065bc761b56a5374d77bac2386dcb6e Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 5 Jun 2025 01:58:29 +0530 Subject: [PATCH 16/45] Fix: Foreign key issue in versioning Component permissions --- server/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ee b/server/ee index 0047b9e0c6..c6e11564b5 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 0047b9e0c6d1ea6bed11de4333ac6961ec030eab +Subproject commit c6e11564b5f7ad79aa34fc7bf9ac1b14c8098dad From 65ed3b87282310da42a57a66ec908884f0ed9341 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 5 Jun 2025 10:45:59 +0530 Subject: [PATCH 17/45] Fix: query not running on application load if query not accessible --- frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js | 5 ++++- server/ee | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js index 79acc2c461..7fd9c46af6 100644 --- a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js +++ b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js @@ -420,7 +420,10 @@ export const createDataQuerySlice = (set, get) => ({ const queries = get().dataQuery.queries.modules.canvas; try { for (const query of queries) { - if ((query.options.runOnPageLoad || query.options.run_on_page_load) && isQueryRunnable(query)) { + if ( + (query.options.runOnPageLoad || query.options.run_on_page_load) && + (query.restricted || isQueryRunnable(query)) + ) { await get().queryPanel.runQuery(query.id, query.name, undefined, undefined, {}, false, true, 'canvas'); } } diff --git a/server/ee b/server/ee index c6e11564b5..f59972c5c3 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit c6e11564b5f7ad79aa34fc7bf9ac1b14c8098dad +Subproject commit f59972c5c3c37179b467deeda0d4d3bf81b5c6f9 From 970cfe21c7804af5ca684573293bbe3a9acd5c70 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 5 Jun 2025 12:15:42 +0530 Subject: [PATCH 18/45] Add page, query or component name to the permissions modal --- frontend/ee | 2 +- frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx | 2 ++ frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx | 1 + frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/ee b/frontend/ee index d97f9b8c1c..743e06cd6e 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit d97f9b8c1c0dd54696d3c13fc9dc2227db6c9664 +Subproject commit 743e06cd6e1233d0b98720354bb7b0d0df098f22 diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx index d55864e91c..68a54c2ca4 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx @@ -29,6 +29,7 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { const enableReleasedVersionPopupState = useStore((state) => state.enableReleasedVersionPopupState); const closePageEditPopover = useStore((state) => state.closePageEditPopover); const editingPageId = useStore((state) => state.editingPage?.id); + const editingPageName = useStore((state) => state.editingPage?.name); const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions); @@ -105,6 +106,7 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { { Date: Thu, 5 Jun 2025 14:50:03 +0530 Subject: [PATCH 19/45] Added duplicating logic for query --- .../_stores/slices/dataQuerySlice.js | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js index 7fd9c46af6..3a8dba58c6 100644 --- a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js +++ b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js @@ -1,4 +1,4 @@ -import { dataqueryService } from '@/_services'; +import { dataqueryService, appPermissionService } from '@/_services'; import { getDefaultOptions } from '@/_stores/storeHelper'; import { v4 as uuidv4 } from 'uuid'; import _, { isEmpty, throttle } from 'lodash'; @@ -256,7 +256,6 @@ export const createDataQuerySlice = (set, get) => ({ ) .then((data) => { set((state) => { - state.dataQuery.creatingQueryInProcessId = null; state.dataQuery.queries.modules[moduleId] = [ { ...data, @@ -290,6 +289,42 @@ export const createDataQuerySlice = (set, get) => ({ }; createAppVersionEventHandlers(newEvent, moduleId); }); + + if (queryToClone.permissions && queryToClone.permissions.length !== 0) { + const body = { + type: queryToClone.permissions[0]?.type, + ...(queryToClone.permissions[0]?.type === 'GROUP' + ? { + groups: (queryToClone.permissions[0]?.groups || queryToClone.permissions[0]?.users || []).map( + (group) => group.permissionGroupsId || group.permission_groups_id + ), + } + : { users: queryToClone.permissions[0]?.users.map((user) => user.userId || user.user_id) }), + }; + appPermissionService + .createQueryPermission(appId, data.id, body) + .then((newQuery) => { + const dataQueries = get().dataQuery.queries.modules[moduleId]; + const updatedDataQueries = dataQueries.map((query) => { + if (query.id === data.id) { + return { + ...query, + permissions: newQuery.length === 0 || newQuery.length === undefined ? [] : [newQuery[0]], + }; + } + return query; + }); + get().dataQuery.setQueries(updatedDataQueries); + }) + .catch(() => { + toast.error('Permission could not be created. Please try again!', { + className: 'text-nowrap w-auto mw-100', + }); + }); + } + set((state) => { + state.dataQuery.creatingQueryInProcessId = null; + }); }) .catch((error) => { console.error('error', error); From 5a3170b2d04e458c08471f447861bf090b98860c Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Wed, 11 Jun 2025 10:01:42 +0530 Subject: [PATCH 20/45] Fixed component vanishing issue on version creation and promotion --- .../AppBuilder/Header/CreateVersionModal.jsx | 4 ++- frontend/src/AppBuilder/_hooks/useAppData.js | 4 +-- .../slices/environmentsAndVersionsSlice.js | 2 +- frontend/src/_services/appVersion.service.js | 4 +-- server/ee | 2 +- server/src/modules/versions/controller.v2.ts | 6 ++-- .../versions/interfaces/IControllerV2.ts | 2 +- .../modules/versions/interfaces/IService.ts | 2 +- server/src/modules/versions/repository.ts | 28 +++++++++++++++++++ server/src/modules/versions/service.ts | 2 +- 10 files changed, 43 insertions(+), 13 deletions(-) diff --git a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx index 3add0e6074..689965691a 100644 --- a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx +++ b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx @@ -27,6 +27,7 @@ const CreateVersionModal = ({ appId, setCurrentVersionId, selectedVersion, + currentMode, } = useStore( (state) => ({ createNewVersionAction: state.createNewVersionAction, @@ -39,6 +40,7 @@ const CreateVersionModal = ({ currentVersionId: state.currentVersionId, setCurrentVersionId: state.setCurrentVersionId, selectedVersion: state.selectedVersion, + currentMode: state.currentMode, }), shallow ); @@ -88,7 +90,7 @@ const CreateVersionModal = ({ setIsCreatingVersion(false); setShowCreateAppVersion(false); appVersionService - .getAppVersionData(appId, newVersion.id) + .getAppVersionData(appId, newVersion.id, currentMode) .then((data) => { setCurrentVersionId(newVersion.id); handleCommitOnVersionCreation(data); diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js index 7b74639870..c0cd9124e9 100644 --- a/frontend/src/AppBuilder/_hooks/useAppData.js +++ b/frontend/src/AppBuilder/_hooks/useAppData.js @@ -211,7 +211,7 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v appDataPromise = appService.fetchAppBySlug(slug); } else { appDataPromise = isPreviewForVersion - ? appVersionService.getAppVersionData(appId, versionId) + ? appVersionService.getAppVersionData(appId, versionId, mode) : appService.fetchApp(appId); } @@ -488,7 +488,7 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v if (isEnvChanged) { setEnvironmentLoadingState('loading'); } - appVersionService.getAppVersionData(appId, selectedVersion?.id).then(async (appData) => { + appVersionService.getAppVersionData(appId, selectedVersion?.id, mode).then(async (appData) => { cleanUpStore(); const { should_freeze_editor } = appData; setIsEditorFreezed(should_freeze_editor); diff --git a/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js b/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js index 1edd3994c5..75f89f1e06 100644 --- a/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/environmentsAndVersionsSlice.js @@ -196,7 +196,7 @@ export const createEnvironmentsAndVersionsSlice = (set, get) => ({ }, changeEditorVersionAction: async (appId, versionId, onSuccess, onFailure) => { try { - const data = await appVersionService.getAppVersionData(appId, versionId); + const data = await appVersionService.getAppVersionData(appId, versionId, get().currentMode); const selectedVersion = { id: data.editing_version.id, name: data.editing_version.name, diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js index bda0b7a192..e510fb67d0 100644 --- a/frontend/src/_services/appVersion.service.js +++ b/frontend/src/_services/appVersion.service.js @@ -36,9 +36,9 @@ function promoteEnvironment(appId, versionId, currentEnvironmentId) { }; return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}/promote`, requestOptions).then(handleResponse); } -function getAppVersionData(appId, versionId) { +function getAppVersionData(appId, versionId, mode) { const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' }; - return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}`, requestOptions).then(handleResponse); + return fetch(`${config.apiUrl}/v2/apps/${appId}/versions/${versionId}?mode=${mode}`, requestOptions).then(handleResponse); } function create(appId, versionName, versionFromId, currentEnvironmentId) { diff --git a/server/ee b/server/ee index f59972c5c3..1c46b1b5f0 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit f59972c5c3c37179b467deeda0d4d3bf81b5c6f9 +Subproject commit 1c46b1b5f0150c273adad875a6e8553a00c0f343 diff --git a/server/src/modules/versions/controller.v2.ts b/server/src/modules/versions/controller.v2.ts index 7a3f006883..02e9f91268 100644 --- a/server/src/modules/versions/controller.v2.ts +++ b/server/src/modules/versions/controller.v2.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Put, Query, UseGuards } from '@nestjs/common'; import { VersionService } from './service'; import { InitModule } from '@modules/app/decorators/init-module'; import { MODULES } from '@modules/app/constants/modules'; @@ -26,8 +26,8 @@ export class VersionControllerV2 implements IVersionControllerV2 { @InitFeature(FEATURE_KEY.GET_ONE) @UseGuards(JwtAuthGuard, ValidAppGuard, FeatureAbilityGuard) @Get(':id/versions/:versionId') - getVersion(@User() user: UserEntity, @App() app: AppEntity) { - return this.versionService.getVersion(app, user); + getVersion(@User() user: UserEntity, @App() app: AppEntity, @Query('mode') mode?: string) { + return this.versionService.getVersion(app, user, mode); } @InitFeature(FEATURE_KEY.UPDATE) diff --git a/server/src/modules/versions/interfaces/IControllerV2.ts b/server/src/modules/versions/interfaces/IControllerV2.ts index 2d86353981..f432e9d3dd 100644 --- a/server/src/modules/versions/interfaces/IControllerV2.ts +++ b/server/src/modules/versions/interfaces/IControllerV2.ts @@ -4,7 +4,7 @@ import { App as AppEntity } from '@entities/app.entity'; import { PromoteVersionDto } from '../dto'; export interface IVersionControllerV2 { - getVersion(user: UserEntity, app: AppEntity): Promise; + getVersion(user: UserEntity, app: AppEntity, mode?: string): Promise; updateVersion(user: UserEntity, app: AppEntity, appVersionUpdateDto: AppVersionUpdateDto): Promise; updateGlobalSettings(user: UserEntity, app: AppEntity, appVersionUpdateDto: AppVersionUpdateDto): Promise; promoteVersion(user: UserEntity, app: AppEntity, promoteVersionDto: PromoteVersionDto): Promise; diff --git a/server/src/modules/versions/interfaces/IService.ts b/server/src/modules/versions/interfaces/IService.ts index dc39061efa..5c6ed2d1b4 100644 --- a/server/src/modules/versions/interfaces/IService.ts +++ b/server/src/modules/versions/interfaces/IService.ts @@ -11,7 +11,7 @@ export interface IVersionService { deleteVersion(app: App, user: User): Promise; - getVersion(app: App, user: User): Promise; + getVersion(app: App, user: User, mode?: string): Promise; update(app: App, user: User, appVersionUpdateDto: AppVersionUpdateDto): Promise; diff --git a/server/src/modules/versions/repository.ts b/server/src/modules/versions/repository.ts index 4cd26d0202..ae9a0b2160 100644 --- a/server/src/modules/versions/repository.ts +++ b/server/src/modules/versions/repository.ts @@ -147,6 +147,34 @@ export class VersionRepository extends Repository { }, manager || this.manager); } + async findVersionWithQueryPermissions(id: string, manager?: EntityManager): Promise { + return await dbTransactionWrap(async (manager: EntityManager) => { + const appVersion = await manager + .createQueryBuilder(AppVersion, 'appVersion') + .where('appVersion.id = :id', { id }) + .leftJoinAndSelect('appVersion.app', 'app') + .leftJoinAndSelect('appVersion.dataQueries', 'dataQueries') + .leftJoinAndSelect('dataQueries.dataSource', 'dataSource') + .leftJoinAndSelect('dataQueries.plugins', 'plugins') + .leftJoinAndSelect('plugins.manifestFile', 'manifestFile') + .leftJoinAndSelect('dataQueries.permissions', 'permission') + .leftJoinAndSelect('permission.users', 'queryUser') + .leftJoinAndSelect('queryUser.user', 'user') + .leftJoinAndSelect('queryUser.permissionGroup', 'group') + .getOneOrFail(); + + if (appVersion?.dataQueries) { + for (const query of appVersion?.dataQueries) { + if (query?.plugin) { + query.plugin.manifestFile.data = JSON.parse(decode(query.plugin.manifestFile.data.toString('utf8'))); + } + } + } + + return appVersion; + }, manager || this.manager); + } + getVersionsInApp(appId: string, manager?: EntityManager): Promise { return dbTransactionWrap((manager: EntityManager) => { return manager.find(AppVersion, { diff --git a/server/src/modules/versions/service.ts b/server/src/modules/versions/service.ts index 5ab17efd5c..65ef4e3b56 100644 --- a/server/src/modules/versions/service.ts +++ b/server/src/modules/versions/service.ts @@ -107,7 +107,7 @@ export class VersionService implements IVersionService { }, manager); } - async getVersion(app: App, user: User): Promise { + async getVersion(app: App, user: User, mode?: string): Promise { const versionId = app.appVersions[0].id; const appVersion = await this.versionRepository.findVersion(versionId); From 3e463227588962d247fb47d6af61f4f3e9883716 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 12 Jun 2025 21:49:12 +0530 Subject: [PATCH 21/45] Update submodule reference --- server/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ee b/server/ee index 1c46b1b5f0..e54ae1f5ca 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 1c46b1b5f0150c273adad875a6e8553a00c0f343 +Subproject commit e54ae1f5cabf52e9631f0d2e4cda7f7be3dd1f4f From 001f445708722398d9fda9de7347aa3086256e8e Mon Sep 17 00:00:00 2001 From: Midhun G S Date: Mon, 23 Jun 2025 13:16:35 +0530 Subject: [PATCH 22/45] cypress changes (#13037) Co-authored-by: Ajith KV --- cypress-tests/cypress-ee-platform.config.js | 2 +- cypress-tests/cypress/commands/commands.js | 19 +- .../cypress/constants/selectors/common.js | 2 +- .../cypress/constants/selectors/eeCommon.js | 202 ++++++ .../cypress/constants/selectors/onboarding.js | 4 +- .../cypress/constants/texts/eeCommon.js | 75 +++ .../cypress/constants/texts/manageSSO.js | 5 + .../cypress/constants/texts/onboarding.js | 4 +- .../commonTestcases/userManagment/Login.cy.js | 3 - .../userManagment/Signup.cy.js | 8 + .../userManagment/UserInviteFlow.cy.js | 8 +- .../userManagment/bulkUsersUpload.cy.js | 161 +++-- .../userInviteFlowEdgeCases.cy.js | 56 +- .../workspace/ldapOnboarding.cy.js | 250 ++++++++ .../externalApi/workspace/openId.cy.js | 239 +++++++ .../firstUser/firstUserOnboarding.cy.js | 120 ++-- .../fixtures/bulkUser/3_users_upload.csv | 4 + .../fixtures/bulkUser/3_users_upload_ee.csv | 4 + .../cypress/fixtures/bulkUser/empty_names.csv | 3 + .../fixtures/bulkUser/empty_names_ee.csv | 3 + .../fixtures/bulkUser/limit_exceeded.csv | 252 ++++++++ .../fixtures/bulkUser/limit_exceeded_ee.csv | 252 ++++++++ .../fixtures/bulkUser/missing_email.csv | 3 + .../fixtures/bulkUser/missing_email_ee.csv | 3 + .../fixtures/bulkUser/missing_name.csv | 3 + .../fixtures/bulkUser/missing_name_ee.csv | 3 + .../fixtures/bulkUser/missing_role.csv | 3 + .../fixtures/bulkUser/missing_role_ee.csv | 3 + .../bulkUser/non_existing_group_ee.csv | 3 + .../fixtures/bulkUser/same_email_ee.csv | 4 + .../cypress/support/utils/manageSSO.js | 22 +- .../support/utils/platform/eeCommon.js | 591 ++++++++++++++++++ 32 files changed, 2127 insertions(+), 187 deletions(-) create mode 100644 cypress-tests/cypress/constants/selectors/eeCommon.js create mode 100644 cypress-tests/cypress/constants/texts/eeCommon.js create mode 100644 cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/ldapOnboarding.cy.js create mode 100644 cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/openId.cy.js create mode 100644 cypress-tests/cypress/fixtures/bulkUser/3_users_upload.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/3_users_upload_ee.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/empty_names.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/empty_names_ee.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/limit_exceeded.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/limit_exceeded_ee.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/missing_email.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/missing_email_ee.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/missing_name.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/missing_name_ee.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/missing_role.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/missing_role_ee.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/non_existing_group_ee.csv create mode 100644 cypress-tests/cypress/fixtures/bulkUser/same_email_ee.csv create mode 100644 cypress-tests/cypress/support/utils/platform/eeCommon.js diff --git a/cypress-tests/cypress-ee-platform.config.js b/cypress-tests/cypress-ee-platform.config.js index 02b8c1d952..25aa7f6f15 100644 --- a/cypress-tests/cypress-ee-platform.config.js +++ b/cypress-tests/cypress-ee-platform.config.js @@ -98,7 +98,7 @@ module.exports = defineConfig({ configFile: environment.configFile, specPattern: [ "cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js", - "cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js", + "cypress/e2e/happyPath/platform/commonTestcases/userManagment/*.cy.js", "cypress/e2e/happyPath/platform/eeTestcases/**/*.cy.js", ], numTestsKeptInMemory: 1, diff --git a/cypress-tests/cypress/commands/commands.js b/cypress-tests/cypress/commands/commands.js index 3bb63cc926..39e68da18a 100644 --- a/cypress-tests/cypress/commands/commands.js +++ b/cypress-tests/cypress/commands/commands.js @@ -11,7 +11,7 @@ import { selectAppCardOption } from "Support/utils/common"; const API_ENDPOINT = Cypress.env("environment") === "Community" ? "/api/library_apps" - : "/api/library_apps/"; + : "/api/library_apps"; Cypress.Commands.add( "appUILogin", @@ -226,9 +226,9 @@ Cypress.Commands.add( .invoke("text") .then((text) => { cy.wrap(subject).realType(createBackspaceText(text)), - { - delay: 0, - }; + { + delay: 0, + }; }); } ); @@ -548,7 +548,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => { } }); - function installPlugin(pluginName) { + function installPlugin (pluginName) { cy.get('[data-cy="-list-item"]').eq(1).click(); cy.wait(1000); @@ -621,3 +621,12 @@ Cypress.Commands.add( .and("have.text", `${fieldName} is required`); } ); + +Cypress.Commands.add('ifEnv', (expectedEnvs, callback) => { + const actualEnv = Cypress.env("environment"); + const envArray = Array.isArray(expectedEnvs) ? expectedEnvs : [expectedEnvs]; + + if (envArray.includes(actualEnv)) { + callback(); + } +}); \ No newline at end of file diff --git a/cypress-tests/cypress/constants/selectors/common.js b/cypress-tests/cypress/constants/selectors/common.js index c5785dcef1..06f34baa99 100644 --- a/cypress-tests/cypress/constants/selectors/common.js +++ b/cypress-tests/cypress/constants/selectors/common.js @@ -18,7 +18,7 @@ export const commonSelectors = { canvas: "[data-cy=real-canvas]", appCardOptionsButton: "[data-cy=app-card-menu-icon]", autoSave: "[data-cy=autosave-indicator]", - nameInputFieldd: "[data-cy=name-input-field]", + inputFieldName: "[data-cy=name-input-field]", valueInputFieldd: "[data-cy=value-input-field]", skipButton: ".driver-close-btn", skipInstallationModal: "[data-cy=skip-button]", diff --git a/cypress-tests/cypress/constants/selectors/eeCommon.js b/cypress-tests/cypress/constants/selectors/eeCommon.js new file mode 100644 index 0000000000..de54858f45 --- /dev/null +++ b/cypress-tests/cypress/constants/selectors/eeCommon.js @@ -0,0 +1,202 @@ +import { cyParamName } from "./common"; + +export const commonEeSelectors = { + instanceSettingIcon: '[data-cy="instance-settings-option"]', + auditLogIcon: '[data-cy="audit-log-option"]', + cancelButton: '[data-cy="cancel-button"]', + saveButton: '[data-cy="save-button"]', + pageTitle: '[data-cy="dashboard-section-header"]', + modalTitle: '[data-cy="modal-title"]', + modalCloseButton: '[data-cy="modal-close-button"]', + saveButton: '[data-cy="save-button"]', + cardTitle: '[data-cy="card-title"]', + AddQueryButton: '[data-cy="show-ds-popover-button"]', + promoteButton: '[data-cy="promote-button"]', + settingsIcon: '[data-cy="icon-settings"]', + gitSyncIcon: '[data-cy="git-sync-icon"]', + confirmButton: '[data-cy="confirm-button"]', + importFromGit: '[data-cy="import-from-git-button"]', + searchBar: '[data-cy="query-manager-search-bar"]', + nameHeader: '[data-cy="name-header"]', + modalMessage: '[data-cy="modal-message"]', + paginationSection: '[data-cy="pagination-section"]', + +}; + +export const ssoEeSelector = { + oidc: '[data-cy="openid-connect-sso-card"]', + statusLabel: '[data-cy="status-label"]', + oidcToggle: '[data-cy="openid-toggle-input"] > .slider', + oidcPageElements: { + oidcToggleLabel: '[data-cy="openid-toggle-label"]', + nameLabel: '[data-cy="name-label"]', + clientIdLabel: '[data-cy="client-id-label"]', + clientSecretLabel: '[data-cy="client-secret-label"]', + encryptedLabel: '[data-cy="encripted-label"]', + WellKnownUrlLabel: '[data-cy="well-known-url-label"]', + // redirectUrlLabel: '[data-cy="redirect-url-label"]', + }, + nameInput: '[data-cy="name-input"]', + clientIdInput: '[data-cy="client-id-input"]', + clientSecretInput: '[data-cy="client-secret-input"]', + WellKnownUrlInput: '[data-cy="well-known-url-input"]', + redirectUrl: '[data-cy="redirect-url"]', + copyIcon: '[data-cy="copy-icon]', + oidcSSOText: '[data-cy="oidc-sso-button-text"]', + oidcSSOIcon: '[data-cy="oidc-so-icon"]', + ldapPageElements: { + ldapToggleLabel: '[data-cy="ldap-toggle-label"]', + nameLabel: '[data-cy="name-label"]', + hostLabel: '[data-cy="host-label"]', + portLabel: '[data-cy="port-label"]', + baseDnLabel: '[data-cy="base-dn-label"]', + baseDnHelperText: '[data-cy="base-dn-helper-text"]', + sslLabel: '[data-cy="ssl-label"]', + }, + ldapToggle: '[data-cy="ldap-toggle-input"] > .slider', + hostInput: '[data-cy="host-input"]', + portInput: '[data-cy="port-input"]', + baseDnInput: '[data-cy="base-dn-input"]', + sslToggleInput: '[data-cy="ssl-toggle-input"]', + ldapSSOText: '[data-cy="ldap-sso-button-text"]', + userNameInputLabel: '[data-cy="user-name-input-label"]', + passwordInputLabel: '[data-cy="password-label"]', + passwordInputField: '[data-cy="password-input-field"]', + + samlModalElements: { + toggleLabel: '[data-cy="saml-toggle-label"]', + NameLabel: '[data-cy="name-label"]', + metaDataLabel: '[data-cy="idp-metadata-label"]', + baseDNHelperText: '[data-cy="base-dn-helper-text"]', + groupAttributeLabel: '[data-cy="group-attribute-label"]', + groupAttributeHelperText: '[data-cy="group-attribute-helper-text"]', + } +}; + +export const eeGroupsSelector = { + resourceDs: '[data-cy="resource-datasources"]', + dsCreateCheck: '[data-cy="checkbox-create-ds"]', + dsDeleteCheck: '[data-cy="checkbox-delete-ds"]', + datasourceLink: '[data-cy="datasource-link"]', + dsSearch: '[data-cy="datasource-select-search"]', + AddDsButton: '[data-cy="datasource-add-button"]', + dsNameHeader: '[data-cy="datasource-name-header"]', +}; + +export const instanceSettingsSelector = { + allUsersTab: '[data-cy="all-users-list-item"]', + manageInstanceSettings: '[data-cy="manage-instance-settings-list-item"]', + typeColumnHeader: '[data-cy="users-table-type-column-header"]', + workspaceColumnHeader: '[data-cy="users-table-workspaces-column-header"]', + userName: (userName) => { + return `[data-cy="${cyParamName(userName)}-user-name"]`; + }, + userEmail: (userName) => { + return `[data-cy="${cyParamName(userName)}-user-email"]`; + }, + userType: (userName) => { + return `[data-cy="${cyParamName(userName)}-user-type"]`; + }, + userStatus: (userName) => { + return `[data-cy="${cyParamName(userName)}-user-status"]`; + }, + viewButton: (userName) => { + return `[data-cy="${cyParamName(userName)}-user-view-button"]`; + }, + editButton: (userName) => { + return `[data-cy="${cyParamName(userName)}-user-edit-button"]`; + }, + viewModalNoColumnHeader: '[data-cy="number-column-header"]', + viewModalNameColumnHeader: '[data-cy="name-column-header"]', + viewModalStatusColumnHeader: '[data-cy="status-column-header"]', + archiveAllButton: '[data-cy="archive-all-button"]', + viewModalRow: (workspaceName) => { + return `[data-cy="${cyParamName(workspaceName)}-workspace-row"]>`; + }, + + workspaceName: (workspaceName) => { + return `[data-cy="${cyParamName(workspaceName)}-workspace-name"]`; + }, + userStatusChangeButton: '[data-cy="user-state-change-button"]', + superAdminToggle: '[data-cy="super-admin-form-check-input"]', + superAdminToggleLabel: '[data-cy="super-admin-form-check-label"]', + allowWorkspaceToggle: '[data-cy="form-check-input"]', + allowWorkspaceToggleLabel: '[data-cy="form-check-label"]', + allowWorkspaceHelperText: '[data-cy="instance-settings-help-text"]', + allWorkspaceTab: '[data-cy="all-workspaces-list-item"]', +}; + + +export const multiEnvSelector = { + envContainer: '[data-cy="env-container"]', + currentEnvName: '[data-cy="list-current-env-name"]', + envArrow: '[data-cy="env-arrow"]', + selectedEnvName: '[data-cy="selected-current-env-name"]', + envNameList: '[data-cy="env-name-list"]', + appVersionLabel: '[data-cy="app-version-label"]', + currentVersion: '[data-cy="current-version"]', + createNewVersionButton: '[data-cy="create-new-version-button"]', + fromLabel: '[data-cy="from-label"]', + toLabel: '[data-cy="to-label"]', + currEnvName: '[data-cy="current-env-name"]', + targetEnvName: '[data-cy="target-env-name"]', + stagingLabel: '[data-cy="staging-label"]', + productionLabel: '[data-cy="production-label"]', +}; + +export const whiteLabellingSelectors = { + whiteLabelList: '[data-cy="white-labelling-list-item"]', + appLogoLabel: '[data-cy="app-logo-label"]', + appLogoInput: '[data-cy="input-field-app-logo"]', + appLogoHelpText: '[data-cy="app-logo-help-text"]', + pageTitleLabel: '[data-cy="page-title-label"]', + pageTitleInput: '[data-cy="input-field-page-title"]', + pageTitleHelpText: '[data-cy="page-title-help-text"]', + favIconLabel: '[data-cy="fav-icon-label"]', + favIconInput: '[data-cy="input-field-fav-icon"]', + favIconHelpText: '[data-cy="fav-icon-help-text"]', +}; + +export const gitSyncSelector = { + gitCommitInput: '[data-cy="git-commit-input"]', + commitHelperText: '[data-cy="commit-helper-text"]', + gitRepoInput: '[data-cy="git-repo-input"]', + commitMessageInput: '[data-cy="commit-message-input"]', + lastCommitInput: '[data-cy="las-commit-message"]', + lastCommitVersion: '[data-cy="last-commit-version"]', + autherInfo: '[data-cy="auther-info"]', + commitButton: '[data-cy="commit-button"]', + gitSyncToggleInput: '[data-cy="git-sync-toggle-input"]', + gitSyncApphelperText: '[data-cy="sync-app-helper-text"]', + connectRepoButton: '[data-cy="connect-repo-button"]', + toggleMessage: '[data-cy="toggle-message"]', + sshInput: '[data-cy="git-ssh-input"]', + generateSshButton: '[data-cy="generate-ssh-key-button"', + sshInputHelperText: '[data-cy="git-ssh-input-helper-text"]', + configDeleteButton: '[data-cy="button-config-delete"]', + testConnectionButton: '[data-cy="test-connection-button"]', + sshKey: '[data-cy="ssh-key"]', + deployKeyHelperText: '[data-cy="deploy-key-helper-text"]', + gitRepoLink: '[data-cy="git-repo-link"]', + appNameField: '[data-cy="app-name-field"]', + gitRepoInfo: '[data-cy="git-repo-info"]', + pullButton: '[data-cy="pull-button"]' + + +} + +export const workspaceSelector = { + activelink: '[data-cy="active-link"]', + archivedLik: '[data-cy="archived-link"]', + userStatusChange: '[data-cy="button-user-status-change"]', + workspaceStatusChange: '[data-cy="button-ws-status-change"]', + switchWsModalTitle: '[data-cy="switch-modal-title"]', + switchWsModalMessage: '[data-cy="switch-modal-message"]', + workspaceName: (workspaceName) => { + return `[data-cy="${workspaceName}-workspace-name"]` + }, + workspaceInput: (workspaceName) => { + return `[data-cy="${workspaceName}-workspace-input"]` + }, + +} \ No newline at end of file diff --git a/cypress-tests/cypress/constants/selectors/onboarding.js b/cypress-tests/cypress/constants/selectors/onboarding.js index a7839afa74..cf2404f6f3 100644 --- a/cypress-tests/cypress/constants/selectors/onboarding.js +++ b/cypress-tests/cypress/constants/selectors/onboarding.js @@ -54,8 +54,8 @@ export const onboardingSelectors = { basicPlanTitle: '[data-cy="basic-plan-title"]', planPrice: '[data-cy="plan-price"]', pricePeriod: '[data-cy="price-period"]', - flexibleTitle: '[data-cy="flexible-title"]', - businessTitle: '[data-cy="business-title"]', + flexibleTitle: '[data-cy="pro-title"]', + businessTitle: '[data-cy="team-title"]', enterpriseTitle: '[data-cy="enterprise-title"]', customPricingHeader: '[data-cy="custom-pricing-header"]', noCreditCardBanner: '[data-cy="no-credit-card-banner"]', diff --git a/cypress-tests/cypress/constants/texts/eeCommon.js b/cypress-tests/cypress/constants/texts/eeCommon.js new file mode 100644 index 0000000000..b112762f65 --- /dev/null +++ b/cypress-tests/cypress/constants/texts/eeCommon.js @@ -0,0 +1,75 @@ +export const commonEeText = { + cancelButton: "Cancel", + saveButton: "Save changes", + closeButton: "Close", + defaultWorkspace: "My workspace", +}; + +export const ssoEeText = { + statusLabel: "Disabled", + enabledLabel: "Enabled", + disabledLabel: "Disabled", + oidcPageElements: { + oidcToggleLabel: "OpenID Connect", + nameLabel: "Name", + clientIdLabel: "Client ID", + clientSecretLabel: "Client secretEncrypted", + encryptedLabel: "Encrypted", + WellKnownUrlLabel: "Well known URL", + // redirectUrlLabel: "Redirect URL", + }, + oidcEnabledToast: "Enabled OpenId SSO", + oidcDisabledToast: "Disabled OpenId SSO", + oidcUpdatedToast: "updated SSO configurations", + testName: "Tooljet OIDC", + testclientId: "24567098-mklj8t20za1smb2if.apps.googleusercontent.com", + testclientSecret: "2345-client-id-.apps.googleusercontent.com", + testWellknownUrl: "google.com", + oidcSSOText: "Sign in with Tooljet OIDC", + + ldapPageElements: { + ldapToggleLabel: "LDAP", + nameLabel: "Name", + hostLabel: "Host name", + portLabel: "Port", + baseDnLabel: "Base DN", + baseDnHelperText: "Location without UID or CN", + sslLabel: "SSL", + }, + ldapSSOText: "Sign in with Tooljet LDAP Auth", + userNameInputLabel: "Username", + samlModalElements: { + toggleLabel: "SAML", + NameLabel: "Name", + metaDataLabel: "Identity provider metadata", + baseDNHelperText: + "Ensure the Identity provider metadata is in XML format. You can download it from your IdP's site", + groupAttributeLabel: "Group attribute", + groupAttributeHelperText: + "Define attribute for user-to-group mapping based on the IdP", + }, +}; +export const eeGroupsText = { + resourceDs: "Datasources", + AddDsButton: "Add", + dsNameHeader: "Datasource name", +}; + +export const instanceSettingsText = { + pageTitle: "Settings", + allUsersTab: "All users", + manageInstanceSettings: "Manage instance settings", + typeColumnHeader: "Type", + workspaceColumnHeader: "Workspaces", + superAdminType: "instance", + viewModalTitle: "Workspaces of The Developer", + archiveAllButton: "Archive All", + archiveState: "Archive", + editModalTitle: "Edit user details", + superAdminToggleLabel: "Super admin", + allowWorkspaceToggleLabel: "Allow personal workspace", + allowWorkspaceHelperText: + "This feature will enable users to create their own workspace", + saveButton: "Save", + untitledWorkspace: "Untitled workspace", +}; diff --git a/cypress-tests/cypress/constants/texts/manageSSO.js b/cypress-tests/cypress/constants/texts/manageSSO.js index c1a95e7179..56052be58f 100644 --- a/cypress-tests/cypress/constants/texts/manageSSO.js +++ b/cypress-tests/cypress/constants/texts/manageSSO.js @@ -59,4 +59,9 @@ export const ssoText = { alertText: "Danger zone", disablePasswordHelperText: "Disable password login only if your SSO is configured otherwise you will get locked out", + disablePasswordHelperText: + "Disable password login only if your SSO is configured otherwise you will get locked out", + toggleUpdateToast: (toggle) => { + return `Saved ${toggle} SSO configurations` + } }; diff --git a/cypress-tests/cypress/constants/texts/onboarding.js b/cypress-tests/cypress/constants/texts/onboarding.js index a41a654525..18efeb9006 100644 --- a/cypress-tests/cypress/constants/texts/onboarding.js +++ b/cypress-tests/cypress/constants/texts/onboarding.js @@ -23,8 +23,8 @@ export const onboardingText = { endUserPriceText: "$10", comparePlansText: "Compare plans", basicPlanText: "Basic Plan", - flexibleText: "Flexible", - businessText: "Business", + flexibleText: "Pro", + businessText: "Team", enterpriseText: "Enterprise", customPricingText: "Custom pricing", noCreditCardText: "No credit card required!", diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/Login.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/Login.cy.js index 55688c9222..ad30526421 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/Login.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/Login.cy.js @@ -50,9 +50,6 @@ describe("Login functionality", () => { it("Should be able to login with valid credentials", () => { cy.appUILogin(user.email, user.password); - if (envVar === "Enterprise") { - cy.get(".btn-close").click(); - } cy.get(commonSelectors.settingsIcon).click(); cy.get(dashboardSelector.logoutLink); }); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/Signup.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/Signup.cy.js index 40bc083798..3cc22ce9b8 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/Signup.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/Signup.cy.js @@ -15,11 +15,19 @@ import { } from "Support/utils/selfHostSignUp"; import { onboardingSelectors } from "Selectors/onboarding"; import { logout } from "Support/utils/common"; +import { enableInstanceSignup } from "Support/utils/manageSSO"; describe("User signup", () => { const data = {}; let invitationLink = ""; + before(() => { + cy.ifEnv("Enterprise", () => { + enableInstanceSignup() + }); + + }); + it("Verify the signup flow and UI elements", () => { data.fullName = fake.fullName; data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", ""); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/UserInviteFlow.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/UserInviteFlow.cy.js index 5cdc6c5764..9883fef40d 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/UserInviteFlow.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/UserInviteFlow.cy.js @@ -23,6 +23,8 @@ import { import { groupsSelector } from "Selectors/manageGroups"; import { groupsText } from "Texts/manageGroups"; import { onboardingSelectors } from "Selectors/onboarding"; +import { enableInstanceSignup } from "Support/utils/manageSSO"; + let invitationToken, organizationToken, @@ -36,9 +38,9 @@ const envVar = Cypress.env("environment"); describe("user invite flow cases", () => { beforeEach(() => { cy.defaultWorkspaceLogin(); - if (envVar === "Enterprise") { - cy.get(".btn-close").click(); - } + cy.ifEnv("Enterprise", () => { + enableInstanceSignup() + }); }); it("Should verify the Manage users page", () => { diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/bulkUsersUpload.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/bulkUsersUpload.cy.js index b769f09d64..32e0a6c064 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/bulkUsersUpload.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/bulkUsersUpload.cy.js @@ -1,59 +1,126 @@ import { commonSelectors } from "Selectors/common"; -import { usersText } from "Texts/manageUsers"; import { usersSelector } from "Selectors/manageUsers"; import { groupsSelector } from "Selectors/manageGroups"; import { fake } from "Fixtures/fake"; import * as common from "Support/utils/common"; import { bulkUserUpload } from "Support/utils/manageUsers"; +// Helper to resolve correct test data based on env +const getFile = (fileGroup) => { + const env = Cypress.env("environment"); + return env === "Community" ? fileGroup.default : fileGroup.alt; +}; + + describe("Bulk User Upload", () => { - // Test data configuration const TEST_FILES = { MISSING_NAME: { - path: "cypress/fixtures/bulkUser/without_name.csv", - fileName: "without_name", - error: - "Missing first_name,last_name,groups information in 2 row(s);. No users were uploaded, please update and try again.", + default: { + path: "cypress/fixtures/bulkUser/missing_name.csv", + fileName: "missing_name", + error: + "Missing first_name,last_name,groups information in 2 row(s);. No users were uploaded, please update and try again.", + }, + alt: { + path: "cypress/fixtures/bulkUser/missing_name_ee.csv", + fileName: "missing_name_ee", + error: + "Missing first_name,last_name,groups,metadata,userMetadata information in 2 row(s);. No users were uploaded, please update and try again.", + }, }, MISSING_EMAIL: { - path: "cypress/fixtures/bulkUser/without_email.csv", - fileName: "without_email", - error: - "Missing email,groups information in 2 row(s);. No users were uploaded, please update and try again.", + default: { + path: "cypress/fixtures/bulkUser/missing_email.csv", + fileName: "missing_email", + error: + "Missing email,groups information in 2 row(s);. No users were uploaded, please update and try again.", + }, + alt: { + path: "cypress/fixtures/bulkUser/missing_email_ee.csv", + fileName: "missing_email_ee", + error: + "Missing first_name,last_name,groups,metadata,userMetadata information in 2 row(s);. No users were uploaded, please update and try again.", + }, }, DUPLICATE_EMAIL: { - path: "cypress/fixtures/bulkUser/same_email.csv", - fileName: "same_email", - error: "Duplicate email found. Please provide a unique email address.", - isDuplicate: true, + default: { + path: "cypress/fixtures/bulkUser/same_email.csv", + fileName: "same_email", + error: "Duplicate email found. Please provide a unique email address.", + isDuplicate: true, + }, + alt: { + path: "cypress/fixtures/bulkUser/same_email_ee.csv", + fileName: "same_email_ee", + error: "Duplicate email found. Please provide a unique email address.", + isDuplicate: true, + }, }, EMPTY_NAMES: { - path: "cypress/fixtures/bulkUser/empty_first_and_last_name.csv", - fileName: "empty_first_and_last_name", - error: - "Missing first_name,last_name,groups information in 1 row(s);. No users were uploaded, please update and try again.", + default: { + path: "cypress/fixtures/bulkUser/empty_names.csv", + fileName: "empty_names", + error: + "Missing first_name,last_name,groups information in 1 row(s);. No users were uploaded, please update and try again.", + }, + alt: { + path: "cypress/fixtures/bulkUser/empty_names_ee.csv", + fileName: "empty_names_ee", + error: + "Missing first_name,last_name,groups,metadata,userMetadata information in 1 row(s);. No users were uploaded, please update and try again.", + }, }, LIMIT_EXCEEDED: { - path: "cypress/fixtures/bulkUser/500_invite_users.csv", - fileName: "500_invite_users", - error: "You can only invite 250 users at a time", + default: { + path: "cypress/fixtures/bulkUser/limit_exceeded.csv", + fileName: "limit_exceeded", + error: "You can only invite 250 users at a time", + }, + alt: { + path: "cypress/fixtures/bulkUser/limit_exceeded_ee.csv", + fileName: "limit_exceeded_ee", + error: "You can only invite 250 users at a time", + }, }, MISSING_ROLE: { - path: "cypress/fixtures/bulkUser/without_role.csv", - fileName: "without_role", - error: - "Missing user_role,groups information in 2 row(s);. No users were uploaded, please update and try again.", + default: { + path: "cypress/fixtures/bulkUser/missing_role.csv", + fileName: "missing_role", + error: + "Missing user_role,groups information in 2 row(s);. No users were uploaded, please update and try again.", + }, + alt: { + path: "cypress/fixtures/bulkUser/missing_role_ee.csv", + fileName: "missing_role_ee", + error: + "Missing user_role,groups,metadata,userMetadata information in 2 row(s);. No users were uploaded, please update and try again.", + }, }, NONEXISTENT_GROUP: { - path: "cypress/fixtures/bulkUser/non_existing_group.csv", - fileName: "non_existing_group", - error: "2 groups doesn't exist. No users were uploaded", + default: { + path: "cypress/fixtures/bulkUser/non_existing_group.csv", + fileName: "non_existing_group", + error: "2 groups doesn't exist. No users were uploaded", + }, + alt: { + path: "cypress/fixtures/bulkUser/non_existing_group_ee.csv", + fileName: "non_existing_group_ee", + error: "2 groups doesn't exist. No users were uploaded", + }, }, VALID_USERS: { - path: "cypress/fixtures/bulkUser/3usersupload.csv", - fileName: "3usersupload", - testEmail: "test12@gmail.com", - successMessage: "3 users are being added", + default: { + path: "cypress/fixtures/bulkUser/3_users_upload.csv", + fileName: "3_users_upload", + successMessage: "3 users are being added", + email: "test12@gmail.com", + }, + alt: { + path: "cypress/fixtures/bulkUser/3_users_upload_ee.csv", + fileName: "3_users_upload_ee", + successMessage: "3 users are being added", + email: "test12@gmail.com", + }, }, }; @@ -70,7 +137,6 @@ describe("Bulk User Upload", () => { cy.get(usersSelector.buttonAddUsers).click(); cy.get(usersSelector.buttonUploadCsvFile).click(); - // Test all error cases [ TEST_FILES.MISSING_ROLE, TEST_FILES.MISSING_NAME, @@ -79,7 +145,8 @@ describe("Bulk User Upload", () => { TEST_FILES.EMPTY_NAMES, TEST_FILES.NONEXISTENT_GROUP, TEST_FILES.LIMIT_EXCEEDED, - ].forEach((testCase) => { + ].forEach((testCaseGroup) => { + const testCase = getFile(testCaseGroup); bulkUserUpload( testCase.path, testCase.fileName, @@ -90,32 +157,30 @@ describe("Bulk User Upload", () => { }); it("Should successfully upload valid users", () => { + const file = getFile(TEST_FILES.VALID_USERS); cy.get(usersSelector.buttonAddUsers).click(); cy.get(usersSelector.buttonUploadCsvFile).click(); - cy.get(usersSelector.inputFieldBulkUpload).selectFile( - TEST_FILES.VALID_USERS.path, - { - force: true, - } - ); - cy.get(commonSelectors.fileSelector).should( - "contain", - TEST_FILES.VALID_USERS.fileName - ); + cy.get(usersSelector.inputFieldBulkUpload).selectFile(file.path, { + force: true, + }); + + cy.get(commonSelectors.fileSelector).should("contain", file.fileName); cy.get(usersSelector.buttonUploadUsers).click(); cy.get(".go2072408551") .should("be.visible") - .and("have.text", TEST_FILES.VALID_USERS.successMessage); - common.searchUser("test12@gmail.com"); - cy.contains("td", "test12@gmail.com") + .and("have.text", file.successMessage); + + common.searchUser(file.email); + cy.contains("td", file.email) .parent() .within(() => { cy.get("td small").should("have.text", "invited"); }); + common.navigateToManageGroups(); cy.get(groupsSelector.groupLink("Admin")).click(); cy.get(groupsSelector.usersLink).click(); - cy.contains("test12@gmail.com").should("be.visible"); + cy.contains(file.email).should("be.visible"); }); }); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/userInviteFlowEdgeCases.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/userInviteFlowEdgeCases.cy.js index 29c521a58b..0769f2fbf3 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/userInviteFlowEdgeCases.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/commonTestcases/userManagment/userInviteFlowEdgeCases.cy.js @@ -21,6 +21,7 @@ import { } from "Support/utils/common"; import { onboardingSelectors } from "Selectors/onboarding"; +import { enableInstanceSignup } from "Support/utils/manageSSO"; const data = {}; const envVar = Cypress.env("environment"); @@ -28,9 +29,9 @@ const envVar = Cypress.env("environment"); describe("inviteflow edge cases", () => { beforeEach(() => { cy.defaultWorkspaceLogin(); - if (envVar === "Enterprise") { - cy.get(".btn-close").click(); - } + cy.ifEnv("Enterprise", () => { + enableInstanceSignup(); + }); }); it("Should verify exisiting user invite flow", () => { @@ -69,55 +70,6 @@ describe("inviteflow edge cases", () => { }); }); - it("should verify the user signup after invited in a workspace", () => { - data.firstName = fake.firstName; - data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", ""); - data.signUpName = fake.firstName; - data.workspaceName = fake.companyName; - - enableInstanceSignUp(); - setSignupStatus(true); - navigateToManageUsers(); - fillUserInviteForm(data.firstName, data.email); - cy.get(usersSelector.buttonInviteUsers).click(); - cy.apiLogout(); - - cy.visit("/"); - cy.get(commonSelectors.createAnAccountLink).click(); - SignUpPageElements(); - cy.wait(3000); - cy.clearAndType(onboardingSelectors.nameInput, data.signUpName); - cy.clearAndType(onboardingSelectors.signupEmailInput, data.email); - cy.clearAndType( - onboardingSelectors.loginPasswordInput, - commonText.password - ); - cy.get(commonSelectors.signUpButton).click(); - cy.wait(1000); - signUpLink(data.email); - if (envVar === "Enterprise") { - verifyOnboardingQuestions(data.workspaceName); - cy.wait(1000); - cy.get(commonSelectors.skipbutton).click(); - cy.backToApps(); - } - cy.wait(1000); - visitWorkspaceInvitation(data.email, "My workspace"); - cy.clearAndType(onboardingSelectors.signupEmailInput, data.email); - cy.clearAndType(onboardingSelectors.loginPasswordInput, usersText.password); - cy.get(onboardingSelectors.signInButton).click(); - cy.wait(3000); - cy.get(commonSelectors.invitedUserName).verifyVisibleElement( - "have.text", - data.signUpName - ); - cy.get(commonSelectors.acceptInviteButton).click(); - cy.get(commonSelectors.workspaceName).verifyVisibleElement( - "have.text", - "My workspace" - ); - }); - it("should verify the user signup with same creds after invited in a workspace", () => { data.firstName = fake.firstName; data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", ""); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/ldapOnboarding.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/ldapOnboarding.cy.js new file mode 100644 index 0000000000..7cc5692226 --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/ldapOnboarding.cy.js @@ -0,0 +1,250 @@ +import { commonSelectors } from "Selectors/common"; +import { commonEeSelectors, ssoEeSelector } from "Selectors/eeCommon"; +import { ssoEeText } from "Texts/eeCommon"; +import { setSSOStatus, setSignupStatus } from "Support/utils/manageSSO"; +import { usersText } from "Texts/manageUsers"; +import { fake } from "Fixtures/fake"; +import { + logout, + navigateToManageSSO, + navigateToManageUsers, + searchUser, + pinInspector, + navigateToAppEditor, + navigateToManageGroups, +} from "Support/utils/common"; +import { ssoText } from "Texts/manageSSO"; +import { enableToggle, disableToggle } from "Support/utils/platform/eeCommon"; +import { setupAndUpdateRole } from "Support/utils/manageGroups"; + +describe("LDAP flow", () => { + const TEST_DATA = { + appName: `${fake.companyName} App`, + ldapUser: { + username: "Hubert J. Farnsworth", + password: "professor", + email: "professor@planetexpress.com", + }, + ldapConfig: { + name: "Tooljet LDAP Auth", + host: Cypress.env("ldap_host"), + port: "10389", + baseDn: Cypress.env("ldap_base_dn"), + }, + }; + + const ldapLogin = ( + username = TEST_DATA.ldapUser.username, + password = TEST_DATA.ldapUser.password + ) => { + cy.get(ssoEeSelector.ldapSSOText).click(); + cy.clearAndType(commonSelectors.inputFieldName, username); + cy.clearAndType(ssoEeSelector.passwordInputField, password); + cy.get(commonSelectors.signUpButton).click(); + }; + + const toggleUserArchiveStatus = (shouldArchive = true) => { + navigateToManageUsers(); + searchUser(TEST_DATA.ldapUser.email); + cy.get('[data-cy="user-actions-button"]').click(); + cy.get('[data-cy="archive-button"]').click(); + + const expectedToast = shouldArchive + ? usersText.archivedToast + : usersText.unarchivedToast; + cy.verifyToastMessage(commonSelectors.toastMessage, expectedToast); + + if (shouldArchive) { + cy.contains("td", TEST_DATA.ldapUser.email) + .parent() + .within(() => { + cy.get("td small").should("have.text", usersText.archivedStatus); + }); + } + }; + + beforeEach(() => { + cy.visit("/"); + cy.appUILogin(); + }); + + it("Verify complete LDAP flow: UI, user onboarding, inspector SSO info, and archive functionality", () => { + cy.intercept("GET", "api/library_apps").as("apps"); + + // ========== SECTION 1: LDAP Configuration and UI Verification ========== + setSSOStatus("My workspace", "ldap", false); + navigateToManageSSO(); + cy.wait(1000); + + cy.get('[data-cy="ldap-sso-card"]') + .verifyVisibleElement("have.text", "LDAP") + .click(); + + cy.get(ssoEeSelector.ldapToggle).should("be.visible"); + + for (const element in ssoEeSelector.ldapPageElements) { + cy.get(ssoEeSelector.ldapPageElements[element]).verifyVisibleElement( + "have.text", + ssoEeText.ldapPageElements[element] + ); + } + + const formElements = [ + ssoEeSelector.statusLabel, + ssoEeSelector.nameInput, + ssoEeSelector.hostInput, + ssoEeSelector.portInput, + ssoEeSelector.baseDnInput, + ssoEeSelector.sslToggleInput, + ]; + + formElements.forEach((selector) => { + cy.get(selector).should("be.visible"); + }); + + cy.get(commonSelectors.cancelButton) + .eq(1) + .verifyVisibleElement("have.text", "Cancel"); + cy.get(commonEeSelectors.saveButton) + .eq(1) + .verifyVisibleElement("have.text", "Save changes"); + + enableToggle(ssoEeSelector.sslToggleInput); + cy.get(ssoEeSelector.ldapPageElements.sslLabel) + .eq(1) + .verifyVisibleElement("have.text", "SSL certificate"); + cy.get(".css-1x65k0v-control").should("be.visible"); + + cy.clearAndType(ssoEeSelector.nameInput, TEST_DATA.ldapConfig.name); + cy.clearAndType(ssoEeSelector.hostInput, TEST_DATA.ldapConfig.host); + cy.clearAndType(ssoEeSelector.portInput, TEST_DATA.ldapConfig.port); + cy.clearAndType(ssoEeSelector.baseDnInput, TEST_DATA.ldapConfig.baseDn); + + cy.get(ssoEeSelector.sslToggleInput).uncheck(); + cy.get(ssoEeSelector.ldapToggle).click(); + disableToggle(ssoEeSelector.sslToggleInput); + + cy.get(commonEeSelectors.saveButton).eq(1).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + ssoText.toggleUpdateToast("LDAP") + ); + cy.get(commonSelectors.cancelButton).eq(1).click(); + logout(); + + // ========== SECTION 2: LDAP Login Page and User Onboarding ========== + cy.get(ssoEeSelector.ldapSSOText).verifyVisibleElement( + "have.text", + ssoEeText.ldapSSOText + ); + + cy.get(ssoEeSelector.ldapSSOText).click(); + + const loginPageElements = [ + { selector: '[data-cy="key-logo"]', assertion: "be.visible" }, + { + selector: ssoEeSelector.userNameInputLabel, + text: ssoEeText.userNameInputLabel, + }, + { selector: commonSelectors.inputFieldName, assertion: "be.visible" }, + { selector: ssoEeSelector.passwordInputLabel, text: "Password" }, + { selector: ssoEeSelector.passwordInputField, assertion: "be.visible" }, + { selector: commonSelectors.signUpButton, text: "Sign in" }, + ]; + + loginPageElements.forEach((element) => { + if (element.text) { + cy.get(element.selector).verifyVisibleElement( + "have.text", + element.text + ); + } else { + cy.get(element.selector).should(element.assertion); + } + }); + + // Test failed login (user doesn't exist in workspace) + cy.clearAndType( + commonSelectors.inputFieldName, + TEST_DATA.ldapUser.username + ); + cy.clearAndType( + ssoEeSelector.passwordInputField, + TEST_DATA.ldapUser.password + ); + cy.get(commonSelectors.signUpButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + "LDAP login failed - User does not exist in the workspace" + ); + + cy.defaultWorkspaceLogin(); + setSignupStatus(true); + logout(); + + ldapLogin(); + cy.get(commonSelectors.pageSectionHeader).verifyVisibleElement( + "have.text", + "Applications" + ); + logout(); + + // ========== SECTION 3: Setup App and User Permissions for Inspector Test ========== + cy.defaultWorkspaceLogin(); + cy.apiCreateApp(TEST_DATA.appName); + + navigateToManageGroups(); + setupAndUpdateRole("End-user", "Builder", TEST_DATA.ldapUser.email); + logout(); + + // ========== SECTION 4: Verify SSO User Info in Inspector ========== + ldapLogin(); + + cy.wait("@apps"); + cy.wait(1000); + + navigateToAppEditor(TEST_DATA.appName); + pinInspector(); + + const inspectorPath = [ + '[data-cy="inspector-node-globals"] > .node-key', + '[data-cy="inspector-node-currentuser"] > .node-key', + '[data-cy="inspector-node-ssouserinfo"] > .node-key', + '[data-cy="inspector-node-mail"] > .node-key', + ]; + + inspectorPath.forEach((selector) => cy.get(selector).click()); + + cy.get('[data-cy="inspector-node-0"] > .mx-2').verifyVisibleElement( + "have.text", + `"${TEST_DATA.ldapUser.email}"` + ); + cy.backToApps(); + logout(); + + // ========== SECTION 5: Archive/Unarchive Functionality ========== + cy.defaultWorkspaceLogin(); + + // Archive user and verify status + toggleUserArchiveStatus(true); + logout(); + + ldapLogin(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + "LDAP login failed - User is archived in the workspace" + ); + + // Unarchive user + cy.go("back"); + cy.appUILogin(); + toggleUserArchiveStatus(false); + logout(); + + ldapLogin(); + cy.get(commonSelectors.pageSectionHeader).verifyVisibleElement( + "have.text", + "Applications" + ); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/openId.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/openId.cy.js new file mode 100644 index 0000000000..d304d76a07 --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/openId.cy.js @@ -0,0 +1,239 @@ +import * as common from "Support/utils/common"; +import { ssoText } from "Texts/manageSSO"; +import { + inviteUser, + WorkspaceInvitationLink, +} from "Support/utils/platform/eeCommon.js"; +import { commonSelectors } from "Selectors/common"; +import { + commonEeSelectors, + ssoEeSelector, + instanceSettingsSelector, +} from "Selectors/eeCommon"; +import { commonEeText } from "Texts/eeCommon"; +import { + setSignupStatus, + defaultSSO, + deleteOrganisationSSO, +} from "Support/utils/manageSSO"; +import { confirmInviteElements } from "Support/utils/manageUsers"; +import { usersText } from "Texts/manageUsers"; +import { usersSelector } from "Selectors/manageUsers"; + +import { fetchAndVisitInviteLink } from "Support/utils/manageUsers"; +import { enableInstanceSignup } from "Support/utils/manageSSO"; + +describe("Verify OIDC user onboarding", () => { + const envVar = Cypress.env("environment"); + + beforeEach(() => { + cy.defaultWorkspaceLogin(); + cy.intercept("GET", "api/library_apps").as("apps"); + cy.wait(2000); + defaultSSO(true); + }); + + it("Verify user onboarding using workspace OIDC", () => { + deleteOrganisationSSO("My workspace", ["openid"]); + common.navigateToManageSSO(); + defaultSSO(false); + setSignupStatus(false); + cy.wait(1000); + + cy.get(ssoEeSelector.oidc).click(); + cy.get(ssoEeSelector.oidcToggle).click(); + cy.clearAndType(ssoEeSelector.nameInput, "Tooljet OIDC"); + cy.clearAndType( + ssoEeSelector.clientIdInput, + Cypress.env("SSO_OPENID_CLIENT_ID") + ); + cy.clearAndType( + ssoEeSelector.clientSecretInput, + Cypress.env("SSO_OPENID_CLIENT_SECRET") + ); + cy.clearAndType( + ssoEeSelector.WellKnownUrlInput, + Cypress.env("SSO_OPENID_WELL_KNOWN_URL") + ); + cy.get(commonEeSelectors.saveButton).eq(1).click(); + cy.get('[data-cy="enable-button"]').click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + ssoText.toggleUpdateToast("OpenID") + ); + + cy.apiLogout(); + cy.visit("/login/my-workspace"); + cy.get(ssoEeSelector.oidcSSOText).verifyVisibleElement( + "have.text", + "Sign in with Tooljet OIDC" + ); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".superadmin-button").click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + "Open ID login failed - User does not exist in the workspace" + ); + + cy.apiLogin(); + setSignupStatus(true); + cy.apiLogout(); + + cy.visit("/login/my-workspace"); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".superadmin-button").click(); + + common.logout(); + + cy.defaultWorkspaceLogin(); + common.navigateToManageUsers(); + common.searchUser("superadmin@tooljet.com"); + + cy.contains("td", "superadmin@tooljet.com") + .parent() + .within(() => { + cy.get("td small").should("have.text", usersText.activeStatus); + }); + + cy.apiLogout(); + cy.visit("/my-workspace"); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".superadmin-button").click(); + }); + + it("Verify invited user onboarding using instance level OIDC", () => { + setSignupStatus(true); + + cy.ifEnv("Enterprise", () => { + enableInstanceSignup(); + }); + + common.navigateToManageUsers(); + inviteUser("user", "user@tooljet.com"); + confirmInviteElements("user@tooljet.com"); + cy.wait(2000); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".user-button").click(); + cy.wait(1000); + + cy.get(commonSelectors.acceptInviteButton).click(); + cy.wait("@apps"); + cy.contains("My workspace").should("be.visible"); + common.logout(); + + cy.defaultWorkspaceLogin(); + setSignupStatus(false); + + common.navigateToManageUsers(); + cy.wait(500); + inviteUser("user", "userthree@tooljet.com"); + cy.wait(2000); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".user-four-button").click(); + + cy.get(commonSelectors.toastMessage) + .should("be.visible") + .and( + "have.text", + "Open ID login failed - Invalid Email: Please use the email address provided in the invitation." + ); + cy.wait(500); + cy.defaultWorkspaceLogin(); + + setSignupStatus(true); + fetchAndVisitInviteLink("userthree@tooljet.com"); + cy.wait(2000); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".user-four-button").click(); + cy.get(commonSelectors.toastMessage) + .should("be.visible") + .and( + "have.text", + "Open ID login failed - Invalid Email: Please use the email address provided in the invitation." + ); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".superadmin-button").click(); + cy.get(commonSelectors.toastMessage) + .should("be.visible") + .and( + "have.text", + "Open ID login failed - Invalid Email: Please use the email address provided in the invitation." + ); + }); + + if (envVar === "Enterprise") { + it("Verify user onboarding using instance level OIDC", () => { + enableInstanceSignup(); + cy.apiLogout(); + + cy.visit("/"); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".admin-button").click(); + cy.wait(3000); + common.logout(); + + cy.defaultWorkspaceLogin(); + cy.get(commonSelectors.settingsIcon).click(); + cy.get(commonEeSelectors.instanceSettingIcon).click(); + cy.clearAndType(commonSelectors.inputUserSearch, "admin@tooljet.com"); + + cy.get(instanceSettingsSelector.userStatus("admin")).verifyVisibleElement( + "have.text", + usersText.activeStatus + ); + + cy.apiLogout(); + cy.visit("/"); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".admin-button").click(); + }); + } + + + it("Verify archived user login using OIDC", () => { + setSignupStatus(true); + cy.ifEnv("Enterprise", () => { + enableInstanceSignup(); + }); + common.navigateToManageUsers(); + cy.get(usersSelector.buttonAddUsers).click(); + cy.get(commonSelectors.inputFieldFullName).type("user two"); + cy.get(commonSelectors.inputFieldEmailAddress).type("usertwo@tooljet.com"); + cy.get(usersSelector.buttonInviteUsers).click(); + WorkspaceInvitationLink("usertwo@tooljet.com"); + + cy.wait(2000); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".user-two-button").click(); + + cy.get(commonSelectors.acceptInviteButton).click(); + cy.wait("@apps"); + cy.contains("My workspace").should("be.visible"); + common.logout(); + + cy.defaultWorkspaceLogin(); + common.navigateToManageUsers(); + common.searchUser("usertwo@tooljet.com"); + cy.get('[data-cy="user-actions-button"]').click(); + cy.get('[data-cy="archive-button"]').click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + usersText.archivedToast + ); + cy.get(instanceSettingsSelector.userStatus("user two"), { + timeout: 9000, + }).should("have.text", usersText.archivedStatus); + cy.apiLogout(); + cy.visit("/my-workspace"); + cy.wait(2000); + cy.get(ssoEeSelector.oidcSSOText).realClick(); + cy.get(".user-two-button").click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + "Open ID login failed - User is archived in the workspace" + ); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js index ebf671e667..0c536c8ec3 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js @@ -18,7 +18,7 @@ describe("Self host onboarding", () => { }); it("verify elements on self host onboarding page", () => { - if (envVar === "Enterprise") { + cy.ifEnv("Enterprise", () => { cy.get(commonSelectors.HostBanner).should("be.visible"); cy.get(commonSelectors.pageLogo).should("be.visible"); cy.get('[data-cy="welcome-to-tooljet!-header"]').verifyVisibleElement( @@ -34,7 +34,7 @@ describe("Self host onboarding", () => { "Set up ToolJet" ); cy.get('[data-cy="set-up-tooljet-button"]').click(); - } + }); const commonElements = [ { selector: commonSelectors.HostBanner }, @@ -76,20 +76,22 @@ describe("Self host onboarding", () => { cy.get(check.selector).verifyVisibleElement("have.text", check.text); }); - if (envVar === "Community") { + cy.ifEnv("Community", () => { cy.get(commonSelectors.signUpTermsHelperText).should(($el) => { expect($el.contents().first().text().trim()).to.eq( // commonText.selfHostSignUpTermsHelperText "By signing up you are agreeing to the" ); }); - } else if (envVar === "Enterprise") { + }); + + cy.ifEnv("Enterprise", () => { cy.get(commonSelectors.signUpTermsHelperText).should(($el) => { expect($el.contents().first().text().trim()).to.eq( "By signing up you are agreeing to the" ); }); - } + }); const links = [ { @@ -116,20 +118,15 @@ describe("Self host onboarding", () => { cy.get(onboardingSelectors.passwordInput).type("password"); cy.get(commonSelectors.continueButton).click(); - if (envVar === "Enterprise") { + cy.ifEnv("Enterprise", () => { bannerElementsVerification(); onboardingStepOne(); - } + }); bannerElementsVerification(); onboardingStepTwo(); - // if (envVar === "Enterprise") { - // bannerElementsVerification(); - // onboardingStepTwo(); - // } - - if (envVar === "Enterprise") { + cy.ifEnv("Enterprise", () => { bannerElementsVerification(); const trialPageTexts = [ @@ -173,7 +170,7 @@ describe("Self host onboarding", () => { cy.get(onboardingSelectors.onPremiseLink) .verifyVisibleElement("have.text", "Click here") .and("have.attr", "href") - .and("equal", "https://www.tooljet.com/pricing?payment=onpremise"); + .and("equal", "https://tooljet.ai/pricing?payment=onpremise"); const planTitles = [ { @@ -196,66 +193,59 @@ describe("Self host onboarding", () => { const prices = [ { selector: `${onboardingSelectors.planPrice}:eq(0)`, text: "$0" }, - { selector: `${onboardingSelectors.planPrice}:eq(1)`, text: "$30" }, + { + selector: '[data-cy="pro-plan-price"]:eq(0)', + text: "$79/monthper builder", + }, + { + selector: '[data-cy="pro-plan-price"]:eq(1)', + text: "$199/monthper builder", + }, + { + selector: `${onboardingSelectors.planToggleLabel}:eq(0)`, + text: "Yearly20% off", + }, + { + selector: `${onboardingSelectors.planToggleLabel}:eq(1)`, + text: "Yearly20% off", + }, ]; prices.forEach((item) => { cy.get(item.selector).should("be.visible").and("have.text", item.text); }); - cy.get(onboardingSelectors.planToggleInput).should("be.visible"); - cy.get(onboardingSelectors.planToggleLabel).verifyVisibleElement( - "have.text", - "Yearly20% off" - ); - cy.get(onboardingSelectors.discountDetails).verifyVisibleElement( - "have.text", - "20% off" - ); + cy.get(onboardingSelectors.planToggleInput).eq(0).should("be.visible"); + cy.get(onboardingSelectors.planToggleInput).eq(1).should("be.visible"); - cy.get(onboardingSelectors.builderPrice).verifyVisibleElement( - "have.text", - "$24" - ); - cy.get('[data-cy="builder-price-period"]').verifyVisibleElement( - "have.text", - onboardingText.priceMonthlyText - ); - cy.get('[data-cy="builder-price-description"]').verifyVisibleElement( - "have.text", - "per builder" - ); - - cy.get(onboardingSelectors.endUserPrice).verifyVisibleElement( - "have.text", - "$8" - ); - cy.get('[data-cy="enduser-price-period"]').verifyVisibleElement( - "have.text", - onboardingText.priceMonthlyText - ); - cy.get('[data-cy="enduser-price-description"]').verifyVisibleElement( - "have.text", - "per end user" - ); - - cy.get(onboardingSelectors.pricingPlanToggle).uncheck({ force: true }); + cy.get(onboardingSelectors.pricingPlanToggle) + .eq(0) + .uncheck({ force: true }); cy.get(onboardingSelectors.planToggleLabel) - .first() + .eq(0) .verifyVisibleElement("have.text", "Monthly20% off"); cy.get(onboardingSelectors.discountDetails) .should("have.css", "text-decoration") .and("include", "line-through"); - cy.get(onboardingSelectors.builderPrice).verifyVisibleElement( - "have.text", - "$30" - ); - cy.get(onboardingSelectors.endUserPrice).verifyVisibleElement( - "have.text", - "$10" - ); + cy.get('[data-cy="pro-plan-price"]') + .eq(0) + .verifyVisibleElement("have.text", "$99/monthper builder"); + + cy.get(onboardingSelectors.pricingPlanToggle) + .eq(1) + .uncheck({ force: true }); + cy.get(onboardingSelectors.planToggleLabel) + .eq(1) + .verifyVisibleElement("have.text", "Monthly20% off"); + cy.get(onboardingSelectors.discountDetails) + .should("have.css", "text-decoration") + .and("include", "line-through"); + + cy.get('[data-cy="pro-plan-price"]') + .eq(1) + .verifyVisibleElement("have.text", "$249/monthper builder"); cy.get(onboardingSelectors.enterpriseTitle).verifyVisibleElement( "have.text", @@ -274,19 +264,11 @@ describe("Self host onboarding", () => { bannerElementsVerification(); onboardingStepThree(); - } + }); cy.get(commonSelectors.skipbutton).click(); cy.backToApps(); - if (envVar === "Enterprise") { - cy.get(".btn-close").click(); - } - - if (envVar === "Enterprise") { - cy.get(".btn-close").click(); - } - logout(); cy.appUILogin(); diff --git a/cypress-tests/cypress/fixtures/bulkUser/3_users_upload.csv b/cypress-tests/cypress/fixtures/bulkUser/3_users_upload.csv new file mode 100644 index 0000000000..9109b69d4e --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/3_users_upload.csv @@ -0,0 +1,4 @@ +First Name,Last Name,Email,User Role,Group +test1,user,test1@gmail.com,Builder, +test2,user,test3@gmail.com,End User, +Test3,Example,test12@gmail.com,Admin, diff --git a/cypress-tests/cypress/fixtures/bulkUser/3_users_upload_ee.csv b/cypress-tests/cypress/fixtures/bulkUser/3_users_upload_ee.csv new file mode 100644 index 0000000000..5a7996a60a --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/3_users_upload_ee.csv @@ -0,0 +1,4 @@ +First Name,Last Name,Email,User Role,Group,Metadata +test1,user,test1@gmail.com,Builder,, +test2,user,test3@gmail.com,End User,, +Test3,Example,test12@gmail.com,Admin,, diff --git a/cypress-tests/cypress/fixtures/bulkUser/empty_names.csv b/cypress-tests/cypress/fixtures/bulkUser/empty_names.csv new file mode 100644 index 0000000000..56fcda708f --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/empty_names.csv @@ -0,0 +1,3 @@ +First Name,Last Name,Email,User Role,Group +,,test12empty@gmail.com,Admin,Admin +Test,Example,test12empty@gmail.com,Builder,Builder diff --git a/cypress-tests/cypress/fixtures/bulkUser/empty_names_ee.csv b/cypress-tests/cypress/fixtures/bulkUser/empty_names_ee.csv new file mode 100644 index 0000000000..5718fe99f2 --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/empty_names_ee.csv @@ -0,0 +1,3 @@ +First Name,Last Name,Email,User Role,Group,Metadata +,,test12empty@gmail.com,Admin,Admin, +Test,Example,test12empty@gmail.com,Builder,Builder, diff --git a/cypress-tests/cypress/fixtures/bulkUser/limit_exceeded.csv b/cypress-tests/cypress/fixtures/bulkUser/limit_exceeded.csv new file mode 100644 index 0000000000..23e3f2e728 --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/limit_exceeded.csv @@ -0,0 +1,252 @@ +First Name,Last Name,Email,User Role,Group +Vijay,Yadav,vjyaav1@gmail.com,Admin, +Vijay,Yadav,vjyaav2@gmail.com,Builder, +Vijay,Yadav,vjyaav3@gmail.com,Builder, +Vijay,Yadav,vjyaav4@gmail.com,End User, +Vijay,Yadav,vjyaav5@gmail.com,End User, +Vijay,Yadav,vjyaav6@gmail.com,End User, +Vijay,Yadav,vjyaav7@gmail.com,End User, +Vijay,Yadav,vjyaav8@gmail.com,End User, +Vijay,Yadav,vjyaav9@gmail.com,End User, +Vijay,Yadav,vjyaav10@gmail.com,End User, +Vijay,Yadav,vjyaav11@gmail.com,End User, +Vijay,Yadav,vjyaav12@gmail.com,End User, +Vijay,Yadav,vjyaav13@gmail.com,End User, +Vijay,Yadav,vjyaav14@gmail.com,End User, +Vijay,Yadav,vjyaav15@gmail.com,End User, +Vijay,Yadav,vjyaav16@gmail.com,End User, +Vijay,Yadav,vjyaav17@gmail.com,End User, +Vijay,Yadav,vjyaav18@gmail.com,End User, +Vijay,Yadav,vjyaav19@gmail.com,End User, +Vijay,Yadav,vjyaav20@gmail.com,End User, +Vijay,Yadav,vjyaav21@gmail.com,End User, +Vijay,Yadav,vjyaav22@gmail.com,End User, +Vijay,Yadav,vjyaav23@gmail.com,End User, +Vijay,Yadav,vjyaav24@gmail.com,End User, +Vijay,Yadav,vjyaav25@gmail.com,End User, +Vijay,Yadav,vjyaav26@gmail.com,End User, +Vijay,Yadav,vjyaav27@gmail.com,End User, +Vijay,Yadav,vjyaav28@gmail.com,End User, +Vijay,Yadav,vjyaav29@gmail.com,End User, +Vijay,Yadav,vjyaav30@gmail.com,End User, +Vijay,Yadav,vjyaav31@gmail.com,End User, +Vijay,Yadav,vjyaav32@gmail.com,End User, +Vijay,Yadav,vjyaav33@gmail.com,End User, +Vijay,Yadav,vjyaav34@gmail.com,End User, +Vijay,Yadav,vjyaav35@gmail.com,End User, +Vijay,Yadav,vjyaav36@gmail.com,End User, +Vijay,Yadav,vjyaav37@gmail.com,End User, +Vijay,Yadav,vjyaav38@gmail.com,End User, +Vijay,Yadav,vjyaav39@gmail.com,End User, +Vijay,Yadav,vjyaav40@gmail.com,End User, +Vijay,Yadav,vjyaav41@gmail.com,End User, +Vijay,Yadav,vjyaav42@gmail.com,End User, +Vijay,Yadav,vjyaav43@gmail.com,End User, +Vijay,Yadav,vjyaav44@gmail.com,End User, +Vijay,Yadav,vjyaav45@gmail.com,End User, +Vijay,Yadav,vjyaav46@gmail.com,End User, +Vijay,Yadav,vjyaav47@gmail.com,End User, +Vijay,Yadav,vjyaav48@gmail.com,End User, +Vijay,Yadav,vjyaav49@gmail.com,End User, +Vijay,Yadav,vjyaav50@gmail.com,End User, +Vijay,Yadav,vjyaav51@gmail.com,End User, +Vijay,Yadav,vjyaav52@gmail.com,End User, +Vijay,Yadav,vjyaav53@gmail.com,End User, +Vijay,Yadav,vjyaav54@gmail.com,End User, +Vijay,Yadav,vjyaav55@gmail.com,End User, +Vijay,Yadav,vjyaav56@gmail.com,End User, +Vijay,Yadav,vjyaav57@gmail.com,End User, +Vijay,Yadav,vjyaav58@gmail.com,End User, +Vijay,Yadav,vjyaav59@gmail.com,End User, +Vijay,Yadav,vjyaav60@gmail.com,End User, +Vijay,Yadav,vjyaav61@gmail.com,End User, +Vijay,Yadav,vjyaav62@gmail.com,End User, +Vijay,Yadav,vjyaav63@gmail.com,End User, +Vijay,Yadav,vjyaav64@gmail.com,End User, +Vijay,Yadav,vjyaav65@gmail.com,End User, +Vijay,Yadav,vjyaav66@gmail.com,End User, +Vijay,Yadav,vjyaav67@gmail.com,End User, +Vijay,Yadav,vjyaav68@gmail.com,End User, +Vijay,Yadav,vjyaav69@gmail.com,End User, +Vijay,Yadav,vjyaav70@gmail.com,End User, +Vijay,Yadav,vjyaav71@gmail.com,End User, +Vijay,Yadav,vjyaav72@gmail.com,End User, +Vijay,Yadav,vjyaav73@gmail.com,End User, +Vijay,Yadav,vjyaav74@gmail.com,End User, +Vijay,Yadav,vjyaav75@gmail.com,End User, +Vijay,Yadav,vjyaav76@gmail.com,End User, +Vijay,Yadav,vjyaav77@gmail.com,End User, +Vijay,Yadav,vjyaav78@gmail.com,End User, +Vijay,Yadav,vjyaav79@gmail.com,End User, +Vijay,Yadav,vjyaav80@gmail.com,End User, +Vijay,Yadav,vjyaav81@gmail.com,End User, +Vijay,Yadav,vjyaav82@gmail.com,End User, +Vijay,Yadav,vjyaav83@gmail.com,End User, +Vijay,Yadav,vjyaav84@gmail.com,End User, +Vijay,Yadav,vjyaav85@gmail.com,End User, +Vijay,Yadav,vjyaav86@gmail.com,End User, +Vijay,Yadav,vjyaav87@gmail.com,End User, +Vijay,Yadav,vjyaav88@gmail.com,End User, +Vijay,Yadav,vjyaav89@gmail.com,End User, +Vijay,Yadav,vjyaav90@gmail.com,End User, +Vijay,Yadav,vjyaav91@gmail.com,End User, +Vijay,Yadav,vjyaav92@gmail.com,End User, +Vijay,Yadav,vjyaav93@gmail.com,End User, +Vijay,Yadav,vjyaav94@gmail.com,End User, +Vijay,Yadav,vjyaav95@gmail.com,End User, +Vijay,Yadav,vjyaav96@gmail.com,End User, +Vijay,Yadav,vjyaav97@gmail.com,End User, +Vijay,Yadav,vjyaav98@gmail.com,End User, +Vijay,Yadav,vjyaav99@gmail.com,End User, +Vijay,Yadav,vjyaav100@gmail.com,End User, +Vijay,Yadav,vjyaav101@gmail.com,End User, +Vijay,Yadav,vjyaav102@gmail.com,End User, +Vijay,Yadav,vjyaav103@gmail.com,End User, +Vijay,Yadav,vjyaav104@gmail.com,End User, +Vijay,Yadav,vjyaav105@gmail.com,End User, +Vijay,Yadav,vjyaav106@gmail.com,End User, +Vijay,Yadav,vjyaav107@gmail.com,End User, +Vijay,Yadav,vjyaav108@gmail.com,End User, +Vijay,Yadav,vjyaav109@gmail.com,End User, +Vijay,Yadav,vjyaav110@gmail.com,End User, +Vijay,Yadav,vjyaav111@gmail.com,End User, +Vijay,Yadav,vjyaav112@gmail.com,End User, +Vijay,Yadav,vjyaav113@gmail.com,End User, +Vijay,Yadav,vjyaav114@gmail.com,End User, +Vijay,Yadav,vjyaav115@gmail.com,End User, +Vijay,Yadav,vjyaav116@gmail.com,End User, +Vijay,Yadav,vjyaav117@gmail.com,End User, +Vijay,Yadav,vjyaav118@gmail.com,End User, +Vijay,Yadav,vjyaav119@gmail.com,End User, +Vijay,Yadav,vjyaav120@gmail.com,End User, +Vijay,Yadav,vjyaav121@gmail.com,End User, +Vijay,Yadav,vjyaav122@gmail.com,End User, +Vijay,Yadav,vjyaav123@gmail.com,End User, +Vijay,Yadav,vjyaav124@gmail.com,End User, +Vijay,Yadav,vjyaav125@gmail.com,End User, +Vijay,Yadav,vjyaav126@gmail.com,End User, +Vijay,Yadav,vjyaav127@gmail.com,End User, +Vijay,Yadav,vjyaav128@gmail.com,End User, +Vijay,Yadav,vjyaav129@gmail.com,End User, +Vijay,Yadav,vjyaav130@gmail.com,End User, +Vijay,Yadav,vjyaav131@gmail.com,End User, +Vijay,Yadav,vjyaav132@gmail.com,End User, +Vijay,Yadav,vjyaav133@gmail.com,End User, +Vijay,Yadav,vjyaav134@gmail.com,End User, +Vijay,Yadav,vjyaav135@gmail.com,End User, +Vijay,Yadav,vjyaav136@gmail.com,End User, +Vijay,Yadav,vjyaav137@gmail.com,End User, +Vijay,Yadav,vjyaav138@gmail.com,End User, +Vijay,Yadav,vjyaav139@gmail.com,End User, +Vijay,Yadav,vjyaav140@gmail.com,End User, +Vijay,Yadav,vjyaav141@gmail.com,End User, +Vijay,Yadav,vjyaav142@gmail.com,End User, +Vijay,Yadav,vjyaav143@gmail.com,End User, +Vijay,Yadav,vjyaav144@gmail.com,End User, +Vijay,Yadav,vjyaav145@gmail.com,End User, +Vijay,Yadav,vjyaav146@gmail.com,End User, +Vijay,Yadav,vjyaav147@gmail.com,End User, +Vijay,Yadav,vjyaav148@gmail.com,End User, +Vijay,Yadav,vjyaav149@gmail.com,End User, +Vijay,Yadav,vjyaav150@gmail.com,End User, +Vijay,Yadav,vjyaav151@gmail.com,End User, +Vijay,Yadav,vjyaav152@gmail.com,End User, +Vijay,Yadav,vjyaav153@gmail.com,End User, +Vijay,Yadav,vjyaav154@gmail.com,End User, +Vijay,Yadav,vjyaav155@gmail.com,End User, +Vijay,Yadav,vjyaav156@gmail.com,End User, +Vijay,Yadav,vjyaav157@gmail.com,End User, +Vijay,Yadav,vjyaav158@gmail.com,End User, +Vijay,Yadav,vjyaav159@gmail.com,End User, +Vijay,Yadav,vjyaav160@gmail.com,End User, +Vijay,Yadav,vjyaav161@gmail.com,End User, +Vijay,Yadav,vjyaav162@gmail.com,End User, +Vijay,Yadav,vjyaav163@gmail.com,End User, +Vijay,Yadav,vjyaav164@gmail.com,End User, +Vijay,Yadav,vjyaav165@gmail.com,End User, +Vijay,Yadav,vjyaav166@gmail.com,End User, +Vijay,Yadav,vjyaav167@gmail.com,End User, +Vijay,Yadav,vjyaav168@gmail.com,End User, +Vijay,Yadav,vjyaav169@gmail.com,End User, +Vijay,Yadav,vjyaav170@gmail.com,End User, +Vijay,Yadav,vjyaav171@gmail.com,End User, +Vijay,Yadav,vjyaav172@gmail.com,End User, +Vijay,Yadav,vjyaav173@gmail.com,End User, +Vijay,Yadav,vjyaav174@gmail.com,End User, +Vijay,Yadav,vjyaav175@gmail.com,End User, +Vijay,Yadav,vjyaav176@gmail.com,End User, +Vijay,Yadav,vjyaav177@gmail.com,End User, +Vijay,Yadav,vjyaav178@gmail.com,End User, +Vijay,Yadav,vjyaav179@gmail.com,End User, +Vijay,Yadav,vjyaav180@gmail.com,End User, +Vijay,Yadav,vjyaav181@gmail.com,End User, +Vijay,Yadav,vjyaav182@gmail.com,End User, +Vijay,Yadav,vjyaav183@gmail.com,End User, +Vijay,Yadav,vjyaav184@gmail.com,End User, +Vijay,Yadav,vjyaav185@gmail.com,End User, +Vijay,Yadav,vjyaav186@gmail.com,End User, +Vijay,Yadav,vjyaav187@gmail.com,End User, +Vijay,Yadav,vjyaav188@gmail.com,End User, +Vijay,Yadav,vjyaav189@gmail.com,End User, +Vijay,Yadav,vjyaav190@gmail.com,End User, +Vijay,Yadav,vjyaav191@gmail.com,End User, +Vijay,Yadav,vjyaav192@gmail.com,End User, +Vijay,Yadav,vjyaav193@gmail.com,End User, +Vijay,Yadav,vjyaav194@gmail.com,End User, +Vijay,Yadav,vjyaav195@gmail.com,End User, +Vijay,Yadav,vjyaav196@gmail.com,End User, +Vijay,Yadav,vjyaav197@gmail.com,End User, +Vijay,Yadav,vjyaav198@gmail.com,End User, +Vijay,Yadav,vjyaav199@gmail.com,End User, +Vijay,Yadav,vjyaav200@gmail.com,End User, +Vijay,Yadav,vjyaav201@gmail.com,End User, +Vijay,Yadav,vjyaav202@gmail.com,End User, +Vijay,Yadav,vjyaav203@gmail.com,End User, +Vijay,Yadav,vjyaav204@gmail.com,End User, +Vijay,Yadav,vjyaav205@gmail.com,End User, +Vijay,Yadav,vjyaav206@gmail.com,End User, +Vijay,Yadav,vjyaav207@gmail.com,End User, +Vijay,Yadav,vjyaav208@gmail.com,End User, +Vijay,Yadav,vjyaav209@gmail.com,End User, +Vijay,Yadav,vjyaav210@gmail.com,End User, +Vijay,Yadav,vjyaav211@gmail.com,End User, +Vijay,Yadav,vjyaav212@gmail.com,End User, +Vijay,Yadav,vjyaav213@gmail.com,End User, +Vijay,Yadav,vjyaav214@gmail.com,End User, +Vijay,Yadav,vjyaav215@gmail.com,End User, +Vijay,Yadav,vjyaav216@gmail.com,End User, +Vijay,Yadav,vjyaav217@gmail.com,End User, +Vijay,Yadav,vjyaav218@gmail.com,End User, +Vijay,Yadav,vjyaav219@gmail.com,End User, +Vijay,Yadav,vjyaav220@gmail.com,End User, +Vijay,Yadav,vjyaav221@gmail.com,End User, +Vijay,Yadav,vjyaav222@gmail.com,End User, +Vijay,Yadav,vjyaav223@gmail.com,End User, +Vijay,Yadav,vjyaav224@gmail.com,End User, +Vijay,Yadav,vjyaav225@gmail.com,End User, +Vijay,Yadav,vjyaav226@gmail.com,End User, +Vijay,Yadav,vjyaav227@gmail.com,End User, +Vijay,Yadav,vjyaav228@gmail.com,End User, +Vijay,Yadav,vjyaav229@gmail.com,End User, +Vijay,Yadav,vjyaav230@gmail.com,End User, +Vijay,Yadav,vjyaav231@gmail.com,End User, +Vijay,Yadav,vjyaav232@gmail.com,End User, +Vijay,Yadav,vjyaav233@gmail.com,End User, +Vijay,Yadav,vjyaav234@gmail.com,End User, +Vijay,Yadav,vjyaav235@gmail.com,End User, +Vijay,Yadav,vjyaav236@gmail.com,End User, +Vijay,Yadav,vjyaav237@gmail.com,End User, +Vijay,Yadav,vjyaav238@gmail.com,End User, +Vijay,Yadav,vjyaav239@gmail.com,End User, +Vijay,Yadav,vjyaav240@gmail.com,End User, +Vijay,Yadav,vjyaav241@gmail.com,End User, +Vijay,Yadav,vjyaav242@gmail.com,End User, +Vijay,Yadav,vjyaav243@gmail.com,End User, +Vijay,Yadav,vjyaav244@gmail.com,End User, +Vijay,Yadav,vjyaav245@gmail.com,End User, +Vijay,Yadav,vjyaav246@gmail.com,End User, +Vijay,Yadav,vjyaav247@gmail.com,End User, +Vijay,Yadav,vjyaav248@gmail.com,End User, +Vijay,Yadav,vjyaav249@gmail.com,End User, +Vijay,Yadav,vjyaav250@gmail.com,End User, +Vijay,Yadav,vjyaav251@gmail.com,End User, \ No newline at end of file diff --git a/cypress-tests/cypress/fixtures/bulkUser/limit_exceeded_ee.csv b/cypress-tests/cypress/fixtures/bulkUser/limit_exceeded_ee.csv new file mode 100644 index 0000000000..5d5de9460f --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/limit_exceeded_ee.csv @@ -0,0 +1,252 @@ +First Name,Last Name,Email,User Role,Group,Metadata +Vijay,Yadav,vjyaav1@gmail.com,Admin,, +Vijay,Yadav,vjyaav2@gmail.com,Builder,, +Vijay,Yadav,vjyaav3@gmail.com,Builder,, +Vijay,Yadav,vjyaav4@gmail.com,End User,, +Vijay,Yadav,vjyaav5@gmail.com,End User,, +Vijay,Yadav,vjyaav6@gmail.com,End User,, +Vijay,Yadav,vjyaav7@gmail.com,End User,, +Vijay,Yadav,vjyaav8@gmail.com,End User,, +Vijay,Yadav,vjyaav9@gmail.com,End User,, +Vijay,Yadav,vjyaav10@gmail.com,End User,, +Vijay,Yadav,vjyaav11@gmail.com,End User,, +Vijay,Yadav,vjyaav12@gmail.com,End User,, +Vijay,Yadav,vjyaav13@gmail.com,End User,, +Vijay,Yadav,vjyaav14@gmail.com,End User,, +Vijay,Yadav,vjyaav15@gmail.com,End User,, +Vijay,Yadav,vjyaav16@gmail.com,End User,, +Vijay,Yadav,vjyaav17@gmail.com,End User,, +Vijay,Yadav,vjyaav18@gmail.com,End User,, +Vijay,Yadav,vjyaav19@gmail.com,End User,, +Vijay,Yadav,vjyaav20@gmail.com,End User,, +Vijay,Yadav,vjyaav21@gmail.com,End User,, +Vijay,Yadav,vjyaav22@gmail.com,End User,, +Vijay,Yadav,vjyaav23@gmail.com,End User,, +Vijay,Yadav,vjyaav24@gmail.com,End User,, +Vijay,Yadav,vjyaav25@gmail.com,End User,, +Vijay,Yadav,vjyaav26@gmail.com,End User,, +Vijay,Yadav,vjyaav27@gmail.com,End User,, +Vijay,Yadav,vjyaav28@gmail.com,End User,, +Vijay,Yadav,vjyaav29@gmail.com,End User,, +Vijay,Yadav,vjyaav30@gmail.com,End User,, +Vijay,Yadav,vjyaav31@gmail.com,End User,, +Vijay,Yadav,vjyaav32@gmail.com,End User,, +Vijay,Yadav,vjyaav33@gmail.com,End User,, +Vijay,Yadav,vjyaav34@gmail.com,End User,, +Vijay,Yadav,vjyaav35@gmail.com,End User,, +Vijay,Yadav,vjyaav36@gmail.com,End User,, +Vijay,Yadav,vjyaav37@gmail.com,End User,, +Vijay,Yadav,vjyaav38@gmail.com,End User,, +Vijay,Yadav,vjyaav39@gmail.com,End User,, +Vijay,Yadav,vjyaav40@gmail.com,End User,, +Vijay,Yadav,vjyaav41@gmail.com,End User,, +Vijay,Yadav,vjyaav42@gmail.com,End User,, +Vijay,Yadav,vjyaav43@gmail.com,End User,, +Vijay,Yadav,vjyaav44@gmail.com,End User,, +Vijay,Yadav,vjyaav45@gmail.com,End User,, +Vijay,Yadav,vjyaav46@gmail.com,End User,, +Vijay,Yadav,vjyaav47@gmail.com,End User,, +Vijay,Yadav,vjyaav48@gmail.com,End User,, +Vijay,Yadav,vjyaav49@gmail.com,End User,, +Vijay,Yadav,vjyaav50@gmail.com,End User,, +Vijay,Yadav,vjyaav51@gmail.com,End User,, +Vijay,Yadav,vjyaav52@gmail.com,End User,, +Vijay,Yadav,vjyaav53@gmail.com,End User,, +Vijay,Yadav,vjyaav54@gmail.com,End User,, +Vijay,Yadav,vjyaav55@gmail.com,End User,, +Vijay,Yadav,vjyaav56@gmail.com,End User,, +Vijay,Yadav,vjyaav57@gmail.com,End User,, +Vijay,Yadav,vjyaav58@gmail.com,End User,, +Vijay,Yadav,vjyaav59@gmail.com,End User,, +Vijay,Yadav,vjyaav60@gmail.com,End User,, +Vijay,Yadav,vjyaav61@gmail.com,End User,, +Vijay,Yadav,vjyaav62@gmail.com,End User,, +Vijay,Yadav,vjyaav63@gmail.com,End User,, +Vijay,Yadav,vjyaav64@gmail.com,End User,, +Vijay,Yadav,vjyaav65@gmail.com,End User,, +Vijay,Yadav,vjyaav66@gmail.com,End User,, +Vijay,Yadav,vjyaav67@gmail.com,End User,, +Vijay,Yadav,vjyaav68@gmail.com,End User,, +Vijay,Yadav,vjyaav69@gmail.com,End User,, +Vijay,Yadav,vjyaav70@gmail.com,End User,, +Vijay,Yadav,vjyaav71@gmail.com,End User,, +Vijay,Yadav,vjyaav72@gmail.com,End User,, +Vijay,Yadav,vjyaav73@gmail.com,End User,, +Vijay,Yadav,vjyaav74@gmail.com,End User,, +Vijay,Yadav,vjyaav75@gmail.com,End User,, +Vijay,Yadav,vjyaav76@gmail.com,End User,, +Vijay,Yadav,vjyaav77@gmail.com,End User,, +Vijay,Yadav,vjyaav78@gmail.com,End User,, +Vijay,Yadav,vjyaav79@gmail.com,End User,, +Vijay,Yadav,vjyaav80@gmail.com,End User,, +Vijay,Yadav,vjyaav81@gmail.com,End User,, +Vijay,Yadav,vjyaav82@gmail.com,End User,, +Vijay,Yadav,vjyaav83@gmail.com,End User,, +Vijay,Yadav,vjyaav84@gmail.com,End User,, +Vijay,Yadav,vjyaav85@gmail.com,End User,, +Vijay,Yadav,vjyaav86@gmail.com,End User,, +Vijay,Yadav,vjyaav87@gmail.com,End User,, +Vijay,Yadav,vjyaav88@gmail.com,End User,, +Vijay,Yadav,vjyaav89@gmail.com,End User,, +Vijay,Yadav,vjyaav90@gmail.com,End User,, +Vijay,Yadav,vjyaav91@gmail.com,End User,, +Vijay,Yadav,vjyaav92@gmail.com,End User,, +Vijay,Yadav,vjyaav93@gmail.com,End User,, +Vijay,Yadav,vjyaav94@gmail.com,End User,, +Vijay,Yadav,vjyaav95@gmail.com,End User,, +Vijay,Yadav,vjyaav96@gmail.com,End User,, +Vijay,Yadav,vjyaav97@gmail.com,End User,, +Vijay,Yadav,vjyaav98@gmail.com,End User,, +Vijay,Yadav,vjyaav99@gmail.com,End User,, +Vijay,Yadav,vjyaav100@gmail.com,End User,, +Vijay,Yadav,vjyaav101@gmail.com,End User,, +Vijay,Yadav,vjyaav102@gmail.com,End User,, +Vijay,Yadav,vjyaav103@gmail.com,End User,, +Vijay,Yadav,vjyaav104@gmail.com,End User,, +Vijay,Yadav,vjyaav105@gmail.com,End User,, +Vijay,Yadav,vjyaav106@gmail.com,End User,, +Vijay,Yadav,vjyaav107@gmail.com,End User,, +Vijay,Yadav,vjyaav108@gmail.com,End User,, +Vijay,Yadav,vjyaav109@gmail.com,End User,, +Vijay,Yadav,vjyaav110@gmail.com,End User,, +Vijay,Yadav,vjyaav111@gmail.com,End User,, +Vijay,Yadav,vjyaav112@gmail.com,End User,, +Vijay,Yadav,vjyaav113@gmail.com,End User,, +Vijay,Yadav,vjyaav114@gmail.com,End User,, +Vijay,Yadav,vjyaav115@gmail.com,End User,, +Vijay,Yadav,vjyaav116@gmail.com,End User,, +Vijay,Yadav,vjyaav117@gmail.com,End User,, +Vijay,Yadav,vjyaav118@gmail.com,End User,, +Vijay,Yadav,vjyaav119@gmail.com,End User,, +Vijay,Yadav,vjyaav120@gmail.com,End User,, +Vijay,Yadav,vjyaav121@gmail.com,End User,, +Vijay,Yadav,vjyaav122@gmail.com,End User,, +Vijay,Yadav,vjyaav123@gmail.com,End User,, +Vijay,Yadav,vjyaav124@gmail.com,End User,, +Vijay,Yadav,vjyaav125@gmail.com,End User,, +Vijay,Yadav,vjyaav126@gmail.com,End User,, +Vijay,Yadav,vjyaav127@gmail.com,End User,, +Vijay,Yadav,vjyaav128@gmail.com,End User,, +Vijay,Yadav,vjyaav129@gmail.com,End User,, +Vijay,Yadav,vjyaav130@gmail.com,End User,, +Vijay,Yadav,vjyaav131@gmail.com,End User,, +Vijay,Yadav,vjyaav132@gmail.com,End User,, +Vijay,Yadav,vjyaav133@gmail.com,End User,, +Vijay,Yadav,vjyaav134@gmail.com,End User,, +Vijay,Yadav,vjyaav135@gmail.com,End User,, +Vijay,Yadav,vjyaav136@gmail.com,End User,, +Vijay,Yadav,vjyaav137@gmail.com,End User,, +Vijay,Yadav,vjyaav138@gmail.com,End User,, +Vijay,Yadav,vjyaav139@gmail.com,End User,, +Vijay,Yadav,vjyaav140@gmail.com,End User,, +Vijay,Yadav,vjyaav141@gmail.com,End User,, +Vijay,Yadav,vjyaav142@gmail.com,End User,, +Vijay,Yadav,vjyaav143@gmail.com,End User,, +Vijay,Yadav,vjyaav144@gmail.com,End User,, +Vijay,Yadav,vjyaav145@gmail.com,End User,, +Vijay,Yadav,vjyaav146@gmail.com,End User,, +Vijay,Yadav,vjyaav147@gmail.com,End User,, +Vijay,Yadav,vjyaav148@gmail.com,End User,, +Vijay,Yadav,vjyaav149@gmail.com,End User,, +Vijay,Yadav,vjyaav150@gmail.com,End User,, +Vijay,Yadav,vjyaav151@gmail.com,End User,, +Vijay,Yadav,vjyaav152@gmail.com,End User,, +Vijay,Yadav,vjyaav153@gmail.com,End User,, +Vijay,Yadav,vjyaav154@gmail.com,End User,, +Vijay,Yadav,vjyaav155@gmail.com,End User,, +Vijay,Yadav,vjyaav156@gmail.com,End User,, +Vijay,Yadav,vjyaav157@gmail.com,End User,, +Vijay,Yadav,vjyaav158@gmail.com,End User,, +Vijay,Yadav,vjyaav159@gmail.com,End User,, +Vijay,Yadav,vjyaav160@gmail.com,End User,, +Vijay,Yadav,vjyaav161@gmail.com,End User,, +Vijay,Yadav,vjyaav162@gmail.com,End User,, +Vijay,Yadav,vjyaav163@gmail.com,End User,, +Vijay,Yadav,vjyaav164@gmail.com,End User,, +Vijay,Yadav,vjyaav165@gmail.com,End User,, +Vijay,Yadav,vjyaav166@gmail.com,End User,, +Vijay,Yadav,vjyaav167@gmail.com,End User,, +Vijay,Yadav,vjyaav168@gmail.com,End User,, +Vijay,Yadav,vjyaav169@gmail.com,End User,, +Vijay,Yadav,vjyaav170@gmail.com,End User,, +Vijay,Yadav,vjyaav171@gmail.com,End User,, +Vijay,Yadav,vjyaav172@gmail.com,End User,, +Vijay,Yadav,vjyaav173@gmail.com,End User,, +Vijay,Yadav,vjyaav174@gmail.com,End User,, +Vijay,Yadav,vjyaav175@gmail.com,End User,, +Vijay,Yadav,vjyaav176@gmail.com,End User,, +Vijay,Yadav,vjyaav177@gmail.com,End User,, +Vijay,Yadav,vjyaav178@gmail.com,End User,, +Vijay,Yadav,vjyaav179@gmail.com,End User,, +Vijay,Yadav,vjyaav180@gmail.com,End User,, +Vijay,Yadav,vjyaav181@gmail.com,End User,, +Vijay,Yadav,vjyaav182@gmail.com,End User,, +Vijay,Yadav,vjyaav183@gmail.com,End User,, +Vijay,Yadav,vjyaav184@gmail.com,End User,, +Vijay,Yadav,vjyaav185@gmail.com,End User,, +Vijay,Yadav,vjyaav186@gmail.com,End User,, +Vijay,Yadav,vjyaav187@gmail.com,End User,, +Vijay,Yadav,vjyaav188@gmail.com,End User,, +Vijay,Yadav,vjyaav189@gmail.com,End User,, +Vijay,Yadav,vjyaav190@gmail.com,End User,, +Vijay,Yadav,vjyaav191@gmail.com,End User,, +Vijay,Yadav,vjyaav192@gmail.com,End User,, +Vijay,Yadav,vjyaav193@gmail.com,End User,, +Vijay,Yadav,vjyaav194@gmail.com,End User,, +Vijay,Yadav,vjyaav195@gmail.com,End User,, +Vijay,Yadav,vjyaav196@gmail.com,End User,, +Vijay,Yadav,vjyaav197@gmail.com,End User,, +Vijay,Yadav,vjyaav198@gmail.com,End User,, +Vijay,Yadav,vjyaav199@gmail.com,End User,, +Vijay,Yadav,vjyaav200@gmail.com,End User,, +Vijay,Yadav,vjyaav201@gmail.com,End User,, +Vijay,Yadav,vjyaav202@gmail.com,End User,, +Vijay,Yadav,vjyaav203@gmail.com,End User,, +Vijay,Yadav,vjyaav204@gmail.com,End User,, +Vijay,Yadav,vjyaav205@gmail.com,End User,, +Vijay,Yadav,vjyaav206@gmail.com,End User,, +Vijay,Yadav,vjyaav207@gmail.com,End User,, +Vijay,Yadav,vjyaav208@gmail.com,End User,, +Vijay,Yadav,vjyaav209@gmail.com,End User,, +Vijay,Yadav,vjyaav210@gmail.com,End User,, +Vijay,Yadav,vjyaav211@gmail.com,End User,, +Vijay,Yadav,vjyaav212@gmail.com,End User,, +Vijay,Yadav,vjyaav213@gmail.com,End User,, +Vijay,Yadav,vjyaav214@gmail.com,End User,, +Vijay,Yadav,vjyaav215@gmail.com,End User,, +Vijay,Yadav,vjyaav216@gmail.com,End User,, +Vijay,Yadav,vjyaav217@gmail.com,End User,, +Vijay,Yadav,vjyaav218@gmail.com,End User,, +Vijay,Yadav,vjyaav219@gmail.com,End User,, +Vijay,Yadav,vjyaav220@gmail.com,End User,, +Vijay,Yadav,vjyaav221@gmail.com,End User,, +Vijay,Yadav,vjyaav222@gmail.com,End User,, +Vijay,Yadav,vjyaav223@gmail.com,End User,, +Vijay,Yadav,vjyaav224@gmail.com,End User,, +Vijay,Yadav,vjyaav225@gmail.com,End User,, +Vijay,Yadav,vjyaav226@gmail.com,End User,, +Vijay,Yadav,vjyaav227@gmail.com,End User,, +Vijay,Yadav,vjyaav228@gmail.com,End User,, +Vijay,Yadav,vjyaav229@gmail.com,End User,, +Vijay,Yadav,vjyaav230@gmail.com,End User,, +Vijay,Yadav,vjyaav231@gmail.com,End User,, +Vijay,Yadav,vjyaav232@gmail.com,End User,, +Vijay,Yadav,vjyaav233@gmail.com,End User,, +Vijay,Yadav,vjyaav234@gmail.com,End User,, +Vijay,Yadav,vjyaav235@gmail.com,End User,, +Vijay,Yadav,vjyaav236@gmail.com,End User,, +Vijay,Yadav,vjyaav237@gmail.com,End User,, +Vijay,Yadav,vjyaav238@gmail.com,End User,, +Vijay,Yadav,vjyaav239@gmail.com,End User,, +Vijay,Yadav,vjyaav240@gmail.com,End User,, +Vijay,Yadav,vjyaav241@gmail.com,End User,, +Vijay,Yadav,vjyaav242@gmail.com,End User,, +Vijay,Yadav,vjyaav243@gmail.com,End User,, +Vijay,Yadav,vjyaav244@gmail.com,End User,, +Vijay,Yadav,vjyaav245@gmail.com,End User,, +Vijay,Yadav,vjyaav246@gmail.com,End User,, +Vijay,Yadav,vjyaav247@gmail.com,End User,, +Vijay,Yadav,vjyaav248@gmail.com,End User,, +Vijay,Yadav,vjyaav249@gmail.com,End User,, +Vijay,Yadav,vjyaav250@gmail.com,End User,, +Vijay,Yadav,vjyaav251@gmail.com,End User,, \ No newline at end of file diff --git a/cypress-tests/cypress/fixtures/bulkUser/missing_email.csv b/cypress-tests/cypress/fixtures/bulkUser/missing_email.csv new file mode 100644 index 0000000000..dd1b93fca2 --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/missing_email.csv @@ -0,0 +1,3 @@ +First Name,Last Name,Email,User Role,Group +test,test,,Admin,Admin +test,test,,Builder,Builder diff --git a/cypress-tests/cypress/fixtures/bulkUser/missing_email_ee.csv b/cypress-tests/cypress/fixtures/bulkUser/missing_email_ee.csv new file mode 100644 index 0000000000..c9fa53181a --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/missing_email_ee.csv @@ -0,0 +1,3 @@ +First Name,Last Name,Email,User Role,Group,Metadata +,,withoutname1@gmail.com,Admin,Admin, +,,withoutname2@gmail.com,Builder,Builder, diff --git a/cypress-tests/cypress/fixtures/bulkUser/missing_name.csv b/cypress-tests/cypress/fixtures/bulkUser/missing_name.csv new file mode 100644 index 0000000000..b4bc6d323d --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/missing_name.csv @@ -0,0 +1,3 @@ +First Name,Last Name,Email,User Role,Group +,,withoutname1@gmail.com,Admin,Admin +,,withoutname2@gmail.com,Builder,Builder diff --git a/cypress-tests/cypress/fixtures/bulkUser/missing_name_ee.csv b/cypress-tests/cypress/fixtures/bulkUser/missing_name_ee.csv new file mode 100644 index 0000000000..c9fa53181a --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/missing_name_ee.csv @@ -0,0 +1,3 @@ +First Name,Last Name,Email,User Role,Group,Metadata +,,withoutname1@gmail.com,Admin,Admin, +,,withoutname2@gmail.com,Builder,Builder, diff --git a/cypress-tests/cypress/fixtures/bulkUser/missing_role.csv b/cypress-tests/cypress/fixtures/bulkUser/missing_role.csv new file mode 100644 index 0000000000..2330a5f7df --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/missing_role.csv @@ -0,0 +1,3 @@ +First Name,Last Name,Email,User Role,Group +Test,Example,test12@gmail.com,, +Test,Example,test13@gmail.com,, diff --git a/cypress-tests/cypress/fixtures/bulkUser/missing_role_ee.csv b/cypress-tests/cypress/fixtures/bulkUser/missing_role_ee.csv new file mode 100644 index 0000000000..aec9c2b880 --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/missing_role_ee.csv @@ -0,0 +1,3 @@ +First Name,Last Name,Email,User Role,Group,Metadata +Test,Example,test12@gmail.com,,, +Test,Example,test13@gmail.com,,, diff --git a/cypress-tests/cypress/fixtures/bulkUser/non_existing_group_ee.csv b/cypress-tests/cypress/fixtures/bulkUser/non_existing_group_ee.csv new file mode 100644 index 0000000000..16620eb8cc --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/non_existing_group_ee.csv @@ -0,0 +1,3 @@ +First Name,Last Name,Email,User Role,Group,Metadata +test,test,demo1@gmail.com,Admin,test, +test,test,demo2@gmail.com,Builder,abc, diff --git a/cypress-tests/cypress/fixtures/bulkUser/same_email_ee.csv b/cypress-tests/cypress/fixtures/bulkUser/same_email_ee.csv new file mode 100644 index 0000000000..634585c86e --- /dev/null +++ b/cypress-tests/cypress/fixtures/bulkUser/same_email_ee.csv @@ -0,0 +1,4 @@ +First Name,Last Name,Email,User Role,Group,Metadata +test,test,demo11@gmail.com,Admin,, +test,test,demo11@gmail.com,Builder,, + diff --git a/cypress-tests/cypress/support/utils/manageSSO.js b/cypress-tests/cypress/support/utils/manageSSO.js index a736afacc8..b8e64e4f56 100644 --- a/cypress-tests/cypress/support/utils/manageSSO.js +++ b/cypress-tests/cypress/support/utils/manageSSO.js @@ -371,7 +371,7 @@ export const defaultSSO = (enable) => { }); }; -export const setSignupStatus = (enable, workspaceName = 'My workspace') => { +export const setSignupStatus = (enable, workspaceName = "My workspace") => { cy.task("dbConnection", { dbconfig: Cypress.env("app_db"), sql: `SELECT id FROM organizations WHERE name = '${workspaceName}'`, @@ -429,3 +429,23 @@ export const resetDomain = () => { }); }); }; + +export const enableInstanceSignup = ( + allowPersonalWorkspace = true, + enableLoginConfig = true, + allowedDomains = "" +) => { + const allowValue = allowPersonalWorkspace ? "true" : "false"; + const loginConfigValue = enableLoginConfig ? "true" : "false"; + + const sql = ` + UPDATE instance_settings SET value = '${allowValue}' WHERE key = 'ALLOW_PERSONAL_WORKSPACE'; + UPDATE instance_settings SET value = '${loginConfigValue}' WHERE key = 'ENABLE_SIGNUP'; + UPDATE instance_settings SET value = '${allowedDomains}' WHERE key = 'ALLOWED_DOMAINS'; + `; + + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql, + }); +}; diff --git a/cypress-tests/cypress/support/utils/platform/eeCommon.js b/cypress-tests/cypress/support/utils/platform/eeCommon.js new file mode 100644 index 0000000000..80db62fca9 --- /dev/null +++ b/cypress-tests/cypress/support/utils/platform/eeCommon.js @@ -0,0 +1,591 @@ +import { + commonEeSelectors, + ssoEeSelector, + instanceSettingsSelector, + multiEnvSelector, + workspaceSelector, +} from "Selectors/eeCommon"; +import { ssoEeText } from "Texts/eeCommon"; +import { commonSelectors, commonWidgetSelector } from "Selectors/common"; +import * as common from "Support/utils/common"; +import { groupsSelector } from "Selectors/manageGroups"; +import { groupsText } from "Texts/manageGroups"; +import { eeGroupsSelector } from "Selectors/eeCommon"; +import { eeGroupsText } from "Texts/eeCommon"; +import { + // verifyOnboardingQuestions, + // verifyCloudOnboardingQuestions, + fetchAndVisitInviteLink, +} from "Support/utils/manageUsers"; +import { commonText } from "Texts/common"; +import { dashboardText } from "Texts/dashboard"; +import { usersText } from "Texts/manageUsers"; +import { usersSelector } from "Selectors/manageUsers"; +import { ssoSelector } from "Selectors/manageSSO"; +import { ssoText } from "Texts/manageSSO"; +// import { appPromote } from "Support/utils/multiEnv"; + +export const oidcSSOPageElements = () => { + cy.get(ssoEeSelector.oidcToggle).click(); + cy.get(ssoSelector.saveButton).eq(1).click(); + cy.get('[data-cy="modal-title"]').verifyVisibleElement( + "have.text", + "Enable OpenID Connect" + ); + cy.get('[data-cy="modal-close-button"]').should("be.visible"); + cy.get('[data-cy="modal-message"]').verifyVisibleElement( + "have.text", + "Enabling OpenID Connect at the workspace level will override any OpenID Connect configurations set at the instance level." + ); + cy.get('[data-cy="confirmation-text"]').verifyVisibleElement( + "have.text", + "Are you sure you want to continue?" + ); + cy.get('[data-cy="cancel-button"]') + .eq(2) + .verifyVisibleElement("have.text", "Cancel"); + cy.get('[data-cy="enable-button"]').verifyVisibleElement( + "have.text", + "Enable" + ); + + cy.get('[data-cy="cancel-button"]').eq(2).click(); + cy.get('[data-cy="status-label"]').verifyVisibleElement( + "have.text", + ssoText.disabledLabel + ); + + cy.get(ssoEeSelector.oidcToggle).click(); + cy.get(ssoSelector.saveButton).eq(1).click(); + cy.get('[data-cy="enable-button"]').click(); + + cy.verifyToastMessage( + commonSelectors.toastMessage, + ssoText.toggleUpdateToast("OpenID") + ); + + cy.get(ssoEeSelector.statusLabel).verifyVisibleElement( + "have.text", + ssoEeText.enabledLabel + ); + + cy.get('[data-cy="redirect-url-label"]').verifyVisibleElement( + "have.text", + ssoText.redirectUrlLabel + ); + cy.get('[data-cy="redirect-url"]').should("be.visible"); + cy.get('[data-cy="copy-icon"]').should("be.visible"); + + cy.get(ssoEeSelector.oidcToggle).click(); + cy.get(ssoSelector.saveButton).eq(1).click(); + // cy.get('[data-cy="enable-button"]').click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + ssoText.toggleUpdateToast("OpenID") + ); + cy.get(ssoSelector.statusLabel).verifyVisibleElement( + "have.text", + ssoText.disabledLabel + ); + + cy.get(ssoEeSelector.oidcToggle).click(); + cy.clearAndType(ssoEeSelector.nameInput, ssoEeText.testName); + cy.clearAndType(ssoEeSelector.clientIdInput, ssoEeText.testclientId); + cy.clearAndType(ssoEeSelector.clientSecretInput, ssoEeText.testclientSecret); + cy.clearAndType(ssoEeSelector.WellKnownUrlInput, ssoEeText.testWellknownUrl); + cy.get(ssoSelector.saveButton).eq(1).click(); + cy.get('[data-cy="enable-button"]').click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + ssoText.toggleUpdateToast("OpenID") + ); + cy.get(ssoEeSelector.nameInput).should("have.value", ssoEeText.testName); + cy.get(ssoEeSelector.clientIdInput).should( + "have.value", + ssoEeText.testclientId + ); + cy.get(ssoEeSelector.clientSecretInput).should( + "have.value", + ssoEeText.testclientSecret + ); + cy.get(ssoEeSelector.WellKnownUrlInput).should( + "have.value", + ssoEeText.testWellknownUrl + ); +}; + +export const resetDsPermissions = () => { + common.navigateToManageGroups(); + cy.wait(200); + cy.get(groupsSelector.permissionsLink).click(); + + cy.get(groupsSelector.appsCreateCheck).then(($el) => { + if ($el.is(":checked")) { + cy.get(groupsSelector.appsCreateCheck).uncheck(); + } + }); + cy.get(eeGroupsSelector.dsCreateCheck).then(($el) => { + if ($el.is(":checked")) { + cy.get(eeGroupsSelector.dsCreateCheck).uncheck(); + } + }); + cy.get(eeGroupsSelector.dsDeleteCheck).then(($el) => { + if ($el.is(":checked")) { + cy.get(eeGroupsSelector.dsDeleteCheck).uncheck(); + } + }); +}; + +export const deleteAssignedDatasources = () => { + common.navigateToManageGroups(); + cy.get('[data-cy="datasource-link"]').click(); + cy.get("body").then(($body) => { + const removeAllButtons = $body.find('[data-cy="remove-button"]'); + if (removeAllButtons.length > 0) { + cy.get('[data-cy="remove-button"]').click({ multiple: true }); + } + }); +}; + +export const userSignUp = (fullName, email, workspaceName) => { + const verificationFunction = + Cypress.env("environment") === "Enterprise" + ? verifyOnboardingQuestions + : verifyCloudOnboardingQuestions; + + let invitationLink = ""; + cy.visit("/"); + cy.wait(500); + cy.get(commonSelectors.createAnAccountLink).realClick(); + cy.clearAndType(commonSelectors.nameInputField, fullName); + cy.clearAndType(commonSelectors.emailInputField, email); + cy.clearAndType(commonSelectors.passwordInputField, commonText.password); + cy.get(commonSelectors.signUpButton).click(); + + cy.wait(500); + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql: `select invitation_token from users where email='${email}';`, + }).then((resp) => { + invitationLink = `/invitations/${resp.rows[0].invitation_token}`; + cy.visit(invitationLink); + cy.get(commonSelectors.setUpToolJetButton).click(); + cy.wait(4000); + + verificationFunction(fullName, workspaceName); + }); +}; + +export const allowPersonalWorkspace = (allow = true) => { + const value = allow ? "true" : "false"; + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql: `UPDATE instance_settings SET value = '${value}' WHERE key = 'ALLOW_PERSONAL_WORKSPACE';`, + }); +}; + +export const addNewUserEE = (firstName, email) => { + common.navigateToManageUsers(); + cy.get(usersSelector.buttonAddUsers).click(); + cy.get(commonSelectors.inputFieldFullName).type(firstName); + cy.get(commonSelectors.inputFieldEmailAddress).type(email); + + cy.get(usersSelector.buttonInviteUsers).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + usersText.userCreatedToast + ); + WorkspaceInvitationLink(email); + cy.clearAndType(commonSelectors.passwordInputField, usersText.password); + cy.get(commonSelectors.signUpButton).click(); + cy.wait(2000); + cy.get(commonSelectors.acceptInviteButton).click(); + cy.get(commonSelectors.workspaceName).verifyVisibleElement( + "have.text", + "My workspace" + ); +}; + +export const inviteUser = (firstName, email) => { + cy.get(usersSelector.buttonAddUsers).click(); + cy.get(commonSelectors.inputFieldFullName).type(firstName); + cy.get(commonSelectors.inputFieldEmailAddress).type(email); + + cy.get(usersSelector.buttonInviteUsers).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + usersText.userCreatedToast + ); + fetchAndVisitInviteLink(email); +}; + +export const defaultWorkspace = () => { + cy.get(".org-select-container").then(($title) => { + if (!$title.text().includes("My workspace")) { + cy.get(commonSelectors.workspaceName).realClick(); + cy.contains("My workspace").realClick(); + cy.wait(2000); + defaultWorkspace(); + } + }); +}; + +export const trunOffAllowPersonalWorkspace = () => { + cy.get(commonSelectors.settingsIcon).click(); + cy.get(commonEeSelectors.instanceSettingIcon).click(); + cy.get(instanceSettingsSelector.manageInstanceSettings).click(); + cy.get(instanceSettingsSelector.allowWorkspaceToggle) + .eq(0) + .then(($el) => { + if ($el.is(":checked")) { + cy.get(instanceSettingsSelector.allowWorkspaceToggle).eq(0).uncheck(); + cy.get(commonEeSelectors.saveButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + "Instance settings have been updated" + ); + } + }); +}; + +export const verifySSOSignUpPageElements = () => { + cy.get(commonSelectors.invitePageHeader).verifyVisibleElement( + "have.text", + "Join ToolJet" + ); + cy.get(commonSelectors.invitePageSubHeader).verifyVisibleElement( + "have.text", + "You are invited to ToolJet." + ); + cy.get(commonSelectors.userNameInputLabel).verifyVisibleElement( + "have.text", + commonText.userNameInputLabel + ); + cy.get(commonSelectors.invitedUserName).should("be.visible"); + cy.get(commonSelectors.emailInputLabel).verifyVisibleElement( + "have.text", + commonText.emailInputLabel + ); + cy.get(commonSelectors.invitedUserEmail).should("be.visible"); + cy.get(commonSelectors.acceptInviteButton).verifyVisibleElement( + "have.text", + commonText.acceptInviteButton + ); + + cy.get(commonSelectors.signUpTermsHelperText).should(($el) => { + expect($el.contents().first().text().trim()).to.eq( + commonText.signUpTermsHelperText + ); + }); + cy.get(commonSelectors.termsOfServiceLink) + .verifyVisibleElement("have.text", commonText.termsOfServiceLink) + .and("have.attr", "href") + .and("equal", "https://www.tooljet.com/terms"); + cy.get(commonSelectors.privacyPolicyLink) + .verifyVisibleElement("have.text", commonText.privacyPolicyLink) + .and("have.attr", "href") + .and("equal", "https://www.tooljet.com/privacy"); +}; + +export const VerifyWorkspaceInvitePageElements = () => { + cy.get(commonSelectors.invitePageHeader).verifyVisibleElement( + "have.text", + commonText.invitePageHeader + ); + cy.get(commonSelectors.invitePageSubHeader).verifyVisibleElement( + "have.text", + commonText.invitePageSubHeader + ); + cy.verifyLabel(commonText.userNameInputLabel); + cy.get(commonSelectors.invitedUserName).should("be.visible"); + cy.verifyLabel(commonText.emailInputLabel); + cy.get(commonSelectors.invitedUserEmail).should("be.visible"); + cy.get(commonSelectors.acceptInviteButton).verifyVisibleElement( + "have.text", + commonText.acceptInviteButton + ); + + cy.get(commonSelectors.signUpTermsHelperText).should(($el) => { + expect($el.contents().first().text().trim()).to.eq( + commonText.signUpTermsHelperText + ); + }); + cy.get(commonSelectors.termsOfServiceLink) + .verifyVisibleElement("have.text", commonText.termsOfServiceLink) + .and("have.attr", "href") + .and("equal", "https://www.tooljet.com/terms"); + cy.get(commonSelectors.privacyPolicyLink) + .verifyVisibleElement("have.text", commonText.privacyPolicyLink) + .and("have.attr", "href") + .and("equal", "https://www.tooljet.com/privacy"); + + cy.get("body").then(($el) => { + if ($el.text().includes("Google")) { + cy.get(ssoSelector.googleSSOText).verifyVisibleElement( + "have.text", + ssoText.googleSignUpText + ); + cy.get(ssoSelector.gitSSOText).verifyVisibleElement( + "have.text", + ssoText.gitSignUpText + ); + cy.get(commonSelectors.onboardingSeperator).should("be.visible"); + } + }); +}; + +export const WorkspaceInvitationLink = (email) => { + let invitationToken, + organizationToken, + workspaceId, + userId, + url = ""; + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql: `select invitation_token from users where email='${email}';`, + }).then((resp) => { + invitationToken = resp.rows[0].invitation_token; + + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql: "select id from organizations where name='My workspace';", + }).then((resp) => { + workspaceId = resp.rows[0].id; + + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql: `select id from users where email='${email}';`, + }).then((resp) => { + userId = resp.rows[0].id; + + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql: `select invitation_token from organization_users where user_id='${userId}';`, + }).then((resp) => { + organizationToken = resp.rows[0].invitation_token; + + url = `/invitations/${invitationToken}/workspaces/${organizationToken}?oid=${workspaceId}`; + common.logout(); + cy.visit(url); + }); + }); + }); + }); +}; + +export const enableDefaultSSO = () => { + common.navigateToManageSSO(); + cy.get("body").then(($el) => { + if (!$el.text().includes("Allowed domains")) { + cy.get(ssoSelector.generalSettingsElements.generalSettings).click(); + } + }); + cy.get(ssoSelector.allowDefaultSSOToggle).then(($el) => { + if (!$el.is(":checked")) { + cy.get(ssoSelector.allowDefaultSSOToggle).check(); + cy.get(ssoSelector.saveButton).click(); + cy.verifyToastMessage(commonSelectors.toastMessage, ssoText.ssoToast); + } + }); +}; + +export const disableSSO = (ssoSelector, toggleSelector) => { + cy.wait(1000); + cy.get(ssoSelector).click(); + cy.get(toggleSelector).then(($el) => { + if ($el.is(":checked")) { + cy.get(toggleSelector).uncheck(); + } + }); +}; + +export const AddDataSourceToGroup = (groupName, dsName) => { + common.navigateToManageGroups(); + cy.get(groupsSelector.groupLink(groupName)).click(); + cy.get(eeGroupsSelector.datasourceLink).click(); + cy.wait(500); + cy.get( + '[data-cy="datasource-select-search"] >> .rmsc > .dropdown-container > .dropdown-heading > .dropdown-heading-value > .gray' + ).click(); + cy.contains(dsName).realClick(); + + cy.get(eeGroupsSelector.AddDsButton).click(); + cy.verifyToastMessage( + commonSelectors.toastMessage, + "Datasources added to the group" + ); +}; + +export const enableToggle = (toggleSelector) => { + cy.get(toggleSelector).then(($el) => { + if (!$el.is(":checked")) { + cy.get(toggleSelector).check(); + } + }); +}; + +export const disableToggle = (toggleSelector) => { + cy.get(toggleSelector).then(($el) => { + if ($el.is(":checked")) { + cy.get(toggleSelector).uncheck(); + } + }); +}; + +export const verifyPromoteModalUI = (versionName, currEnv, targetEnv) => { + cy.get(commonEeSelectors.promoteButton) + .verifyVisibleElement("have.text", " Promote ") + .click(); + cy.get(commonEeSelectors.modalTitle).verifyVisibleElement( + "have.text", + `Promote ${versionName}` + ); + cy.get(commonSelectors.closeButton).should("be.visible"); + cy.get(multiEnvSelector.fromLabel).verifyVisibleElement("have.text", "FROM"); + cy.get(multiEnvSelector.toLabel).verifyVisibleElement("have.text", "TO"); + cy.get(multiEnvSelector.currEnvName).verifyVisibleElement( + "have.text", + currEnv + ); + cy.get('[data-cy="target-env-name"]').verifyVisibleElement( + "have.text", + targetEnv + ); + cy.get('[data-cy="cancel-button"]').verifyVisibleElement( + "have.text", + "Cancel" + ); + cy.get(commonEeSelectors.promoteButton) + .eq(1) + .verifyVisibleElement("have.text", "Promote "); +}; + +export const resetPassword = (email) => { + cy.visit("/"); + cy.get(commonSelectors.forgotPasswordLink).click(); + cy.clearAndType(commonSelectors.emailInputField, email); + cy.get(commonSelectors.resetPasswordLinkButton).click(); + + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql: `select forgot_password_token from users where email='${email}';`, + }).then((resp) => { + const passwordResetLink = `/reset-password/${resp.rows[0].forgot_password_token}`; + cy.visit(passwordResetLink); + }); + cy.wait(500); + + cy.clearAndType(commonSelectors.newPasswordInputField, "Password"); + cy.clearAndType(commonSelectors.confirmPasswordInputField, "Password"); + cy.wait(4000); + cy.get(commonSelectors.resetPasswordButton).click(); + cy.get(commonSelectors.backToLoginButton).click(); +}; + +export const verifyTooltipDisabled = (selector, message) => { + cy.get(selector) + .trigger("mouseover", { force: true }) + .then(() => { + cy.get(".tooltip-inner").last().should("have.text", message); + }); +}; + +export const createAnAppWithSlug = (appName, slug) => { + cy.apiCreateApp(appName); + cy.openApp(); + cy.dragAndDropWidget("Table", 250, 250); + appPromote("development", "release"); + cy.get(commonWidgetSelector.shareAppButton).click(); + cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${slug}`); + cy.wait(2000); + cy.get(commonWidgetSelector.modalCloseButton).click(); +}; + +export const updateLicense = (key) => { + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql: `update instance_settings set value='${key}', updated_at= NOW() where key='LICENSE_KEY';`, + }); +}; + +export const openInstanceSettings = () => { + cy.get(commonSelectors.settingsIcon).click(); + cy.get(commonEeSelectors.instanceSettingIcon).click(); +}; + +export const openUserActionMenu = (email) => { + cy.clearAndType(commonSelectors.inputUserSearch, email); + cy.wait(1000); + cy.get('[data-cy="user-actions-button"]').eq(0).click(); + cy.wait(2000); +}; + +export const archiveWorkspace = (workspaceName) => { + cy.get(instanceSettingsSelector.allWorkspaceTab).click(); + cy.clearAndType(commonEeSelectors.searchBar, workspaceName); + cy.get(workspaceSelector.workspaceStatusChange).eq(0).click(); + cy.get(commonEeSelectors.confirmButton).click(); +}; + +export const passwordToggle = (enable) => { + cy.getCookie("tj_auth_token").then((cookie) => { + cy.request( + { + method: "PATCH", + url: "http://localhost:3000/api/organizations/configs", + headers: { + "Tj-Workspace-Id": Cypress.env("workspaceId"), + Cookie: `tj_auth_token=${cookie.value}`, + }, + body: { type: "form", enabled: enable }, + }, + { log: false } + ).then((response) => { + expect(response.status).to.equal(200); + }); + }); +}; + +export const InstanceSSO = (personalWorkspace, enableSignup, workspaceSSO) => { + allowPersonalWorkspace(personalWorkspace); + + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql: `UPDATE instance_settings SET value = '${enableSignup}' WHERE key = 'ENABLE_SIGNUP';UPDATE instance_settings SET value = '${workspaceSSO}' WHERE key = 'ENABLE_WORKSPACE_LOGIN_CONFIGURATION';`, + }); +}; + +export const resetInstanceDomain = () => { + cy.getCookie("tj_auth_token").then((cookie) => { + cy.request( + { + method: "PATCH", + url: "http://localhost:3000/api/instance-login-configs", + headers: { + "Tj-Workspace-Id": Cypress.env("workspaceId"), + Cookie: `tj_auth_token=${cookie.value}`, + }, + body: { allowedDomains: "" }, + }, + { log: false } + ).then((response) => { + expect(response.status).to.equal(200); + }); + }); +}; + +export const instanceSSOConfig = (allow = true) => { + const value = allow ? "true" : "false"; + + cy.task("dbConnection", { + dbconfig: Cypress.env("app_db"), + sql: `UPDATE sso_configs SET enabled = ${allow} WHERE sso IN ('google', 'git', 'openid')AND organization_id IS NULL;`, + }); +}; + +export const updateInstanceSettings = (key, value) => { + cy.task("updateSetting", { + dbconfig: Cypress.env("app_db"), + sql: `UPDATE instance_settings SET value = ${value} WHERE key = ${key};`, + }); +}; From 7123fc278b04b943d88ed1237ce0a601099dfbb5 Mon Sep 17 00:00:00 2001 From: Adish M <44204658+adishM98@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:08:08 +0530 Subject: [PATCH 23/45] Update merging-pr.yml for rebase submodule current main to base repo (#13067) --- .github/workflows/merging-pr.yml | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/merging-pr.yml b/.github/workflows/merging-pr.yml index bb4f144eac..f091baf0ec 100644 --- a/.github/workflows/merging-pr.yml +++ b/.github/workflows/merging-pr.yml @@ -37,6 +37,40 @@ jobs: env: GH_TOKEN: ${{ secrets.TOKEN_PR }} + + update-submodule-sha: + needs: merge-submodules + if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' + runs-on: ubuntu-latest + + steps: + - name: Checkout base repo + uses: actions/checkout@v4 + with: + repository: ToolJet/ToolJet + token: ${{ secrets.TOKEN_PR }} + ref: main + submodules: recursive + + - name: Update submodules to latest main + run: | + git config user.name "adishM98 Bot" + git config user.email "adish.madhu@gmail.com" + + git submodule update --remote frontend/ee + git submodule update --remote server/ee + + git add frontend/ee server/ee + + if git diff --cached --quiet; then + echo "No submodule updates found." + else + git commit -m "🔄 chore: update submodules to latest main after auto-merge" + git push origin main + fi + env: + GH_TOKEN: ${{ secrets.TOKEN_PR }} + check-submodule-prs: if: github.event.action == 'labeled' && github.event.label.name == 'ready-to-merge' runs-on: ubuntu-latest From 17045befd37c89f4644ffd16e21c7cb5339031af Mon Sep 17 00:00:00 2001 From: Adish M <44204658+adishM98@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:58:46 +0530 Subject: [PATCH 24/45] Update node version in vulnerability automation (#13071) --- .github/workflows/vulnerability-ci.yml | 52 +++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/vulnerability-ci.yml b/.github/workflows/vulnerability-ci.yml index 568ab6df31..ca8c525ace 100644 --- a/.github/workflows/vulnerability-ci.yml +++ b/.github/workflows/vulnerability-ci.yml @@ -24,10 +24,10 @@ jobs: with: ref: refs/heads/main - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm --prefix frontend install @@ -75,10 +75,10 @@ jobs: with: ref: refs/heads/main - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm --prefix server install @@ -106,7 +106,7 @@ jobs: - name: Send Slack Notification run: | - message="### Periodic Security Audit Report Of Server directory\n + message="Periodic Security Audit Report Of Server directory\n Node module vulnerabilities summary:\n 🔴 Critical: ${{ steps.parse-audit.outputs.critical }}\n 🟠 High: ${{ steps.parse-audit.outputs.high }}\n @@ -126,10 +126,10 @@ jobs: with: ref: refs/heads/main - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm --prefix marketplace install @@ -177,10 +177,10 @@ jobs: with: ref: refs/heads/main - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm --prefix plugins install @@ -228,10 +228,10 @@ jobs: with: ref: refs/heads/main - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm --prefix cypress-tests install @@ -279,10 +279,10 @@ jobs: with: ref: refs/heads/main - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm install @@ -330,10 +330,10 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm --prefix frontend install @@ -385,10 +385,10 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm --prefix server install @@ -440,10 +440,10 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm --prefix marketplace install @@ -494,10 +494,10 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm --prefix plugins install @@ -550,10 +550,10 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm --prefix cypress-tests install @@ -606,10 +606,10 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} - - name: Use Node.js 18.18.2 + - name: Use Node.js 22.15.1 uses: actions/setup-node@v3 with: - node-version: 18.18.2 + node-version: 22.15.1 - name: Install dependencies run: npm install @@ -648,4 +648,4 @@ jobs: 🟠 High: ${{ steps.parse-audit.outputs.high }} 🟡 Moderate: ${{ steps.parse-audit.outputs.moderate }} - Please find the JSON file in the [summary page](${{ github.root_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). \ No newline at end of file + Please find the JSON file in the [summary page](${{ github.root_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}). From bed888bd8aac491fff01705e9931901020d8fdf4 Mon Sep 17 00:00:00 2001 From: Adish M <44204658+adishM98@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:43:14 +0530 Subject: [PATCH 25/45] removed docker buildx from cypress-platform.yml (#13075) --- .github/workflows/cypress-platform.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/cypress-platform.yml b/.github/workflows/cypress-platform.yml index d3374604f8..1f6d39997f 100644 --- a/.github/workflows/cypress-platform.yml +++ b/.github/workflows/cypress-platform.yml @@ -37,21 +37,9 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} - # Create Docker Buildx builder with platform configuration - - name: Set up Docker Buildx - run: | - mkdir -p ~/.docker/cli-plugins - curl -SL https://github.com/docker/buildx/releases/download/v0.11.0/buildx-v0.11.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx - chmod a+x ~/.docker/cli-plugins/docker-buildx - docker buildx create --name mybuilder --platform linux/arm64,linux/amd64 - docker buildx use mybuilder - - name: Set DOCKER_CLI_EXPERIMENTAL run: echo "DOCKER_CLI_EXPERIMENTAL=enabled" >> $GITHUB_ENV - - name: use mybuilder buildx - run: docker buildx use mybuilder - - name: Docker Login uses: docker/login-action@v2 with: From a05afd72d74bc361b7201a3717209e2b9697930f Mon Sep 17 00:00:00 2001 From: Mekhla Asopa Date: Tue, 24 Jun 2025 22:16:12 +0530 Subject: [PATCH 26/45] fix all method in rest api --- .../data-source/restAPIHappyPath.cy.js | 130 +++++++++++------- .../cypress/support/utils/restAPI.js | 3 +- 2 files changed, 86 insertions(+), 47 deletions(-) diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/restAPIHappyPath.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/restAPIHappyPath.cy.js index 09559e2ba7..ab3d3993a3 100644 --- a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/restAPIHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/restAPIHappyPath.cy.js @@ -337,7 +337,7 @@ describe("Data source Rest API", () => { `cypress-${data.dataSourceName}-restapi`, "restapi", [ - { key: "url", value: Cypress.env("restAPI_BaseURL") }, + { key: "url", value: "https://jsonplaceholder.typicode.com" }, { key: "auth_type", value: "none" }, { key: "grant_type", value: "authorization_code" }, { key: "add_token_to", value: "header" }, @@ -370,62 +370,100 @@ describe("Data source Rest API", () => { cy.apiCreateApp(`${fake.companyName}-restAPI-CURD-App`); cy.openApp(); createAndRunRestAPIQuery({ - queryName: "get_beeceptor_data", + queryName: "get_all_users", dsName: `cypress-${data.dataSourceName}-restapi`, method: "GET", - urlSuffix: "/api/users", + urlSuffix: "/users", run: true, + expectedResponseShape: { + "0.id": true, + "0.name": true, + "0.email": true, + }, }); createAndRunRestAPIQuery({ queryName: "post_restapi", dsName: `cypress-${data.dataSourceName}-restapi`, method: "POST", headersList: [["Content-Type", "application/json"]], - rawBody: '{"price": 200,"name": "Violin"}', - urlSuffix: "/api/users", - expectedResponseShape: { price: 200, name: "Violin", id: true }, + rawBody: `{ + "name": "Test User", + "username": "testuser", + "email": "test@example.com", + "address": { + "street": "123 Test St", + "city": "Testville", + "zipcode": "12345" + } + }`, + urlSuffix: "/users", + run: true, + expectedResponseShape: { + id: true, + name: "Test User", + email: "test@example.com", + }, }); - cy.readFile("cypress/fixtures/restAPI/storedId.json").then( - (postResponseID) => { - const id1 = postResponseID.id; + const id1 = 1; - createAndRunRestAPIQuery({ - queryName: "put_restapi_id", - dsName: `cypress-${data.dataSourceName}-restapi`, - method: "PUT", - headersList: [["Content-Type", "application/json"]], - rawBody: '{"price": 500,"name": "Guitar"}', - urlSuffix: `/api/users/${id1}`, - expectedResponseShape: { price: 500, name: "Guitar", id: id1 }, - }); - createAndRunRestAPIQuery({ - queryName: "patch_restapi_id", - dsName: `cypress-${data.dataSourceName}-restapi`, - method: "PATCH", - headersList: [["Content-Type", "application/json"]], - rawBody: '{"price": 999 }', - urlSuffix: `/api/users/${id1}`, - run: true, - expectedResponseShape: { price: 999, id: id1 }, - }); - createAndRunRestAPIQuery({ - queryName: "get_restapi_id", - dsName: `cypress-${data.dataSourceName}-restapi`, - method: "GET", - urlSuffix: `/api/users/${id1}`, - run: true, - expectedResponseShape: { price: 999, name: "Guitar", id: id1 }, - }); - createAndRunRestAPIQuery({ - queryName: "delete_restapi_id", - dsName: `cypress-${data.dataSourceName}-restapi`, - method: "DELETE", - urlSuffix: `/api/users/${id1}`, - run: true, - expectedResponseShape: { success: true }, - }); - } - ); + createAndRunRestAPIQuery({ + queryName: "put_restapi_id", + dsName: `cypress-${data.dataSourceName}-restapi`, + method: "PUT", + headersList: [["Content-Type", "application/json"]], + rawBody: `{ + "id": 1, + "name": "Fully Updated User", + "username": "updateduser", + "email": "updated@example.com", + "address": { + "street": "456 Updated St", + "city": "Updatedville", + "zipcode": "54321" + } + }`, + urlSuffix: `/users/${id1}`, + run: true, + expectedResponseShape: { + id: id1, + name: "Fully Updated User", + email: "updated@example.com", + }, + }); + createAndRunRestAPIQuery({ + queryName: "patch_restapi_id", + dsName: `cypress-${data.dataSourceName}-restapi`, + method: "PATCH", + headersList: [["Content-Type", "application/json"]], + rawBody: `{ + "email": "partially.updated@example.com" + }`, + urlSuffix: `/users/${id1}`, + run: true, + expectedResponseShape: { + id: id1, + email: "partially.updated@example.com", + }, + }); + createAndRunRestAPIQuery({ + queryName: "get_restapi_id", + dsName: `cypress-${data.dataSourceName}-restapi`, + method: "GET", + urlSuffix: `/users/${id1}`, + run: true, + expectedResponseShape: { + id: id1, + email: "Sincere@april.biz", + }, + }); + createAndRunRestAPIQuery({ + queryName: "delete_restapi_id", + dsName: `cypress-${data.dataSourceName}-restapi`, + method: "DELETE", + urlSuffix: `/users/${id1}`, + run: true, + expectedResponseShape: {}, + }); cy.apiDeleteApp(`${fake.companyName}-restAPI-CURD-App`); cy.apiDeleteGDS(`cypress-${data.dataSourceName}-restapi`); }); diff --git a/cypress-tests/cypress/support/utils/restAPI.js b/cypress-tests/cypress/support/utils/restAPI.js index f87c8d6f61..dd205d326d 100644 --- a/cypress-tests/cypress/support/utils/restAPI.js +++ b/cypress-tests/cypress/support/utils/restAPI.js @@ -122,10 +122,11 @@ export const createAndRunRestAPIQuery = ({ } if (Array.isArray(responseData)) { responseData.forEach((item) => { - expect(item).to.have.any.keys("id", "name", "price"); + expect(item).to.have.any.keys("id", "name", "username", "email", "address"); }); } if (responseData?.id) { + cy.log(responseData.id) cy.writeFile("cypress/fixtures/restAPI/storedId.json", { id: responseData.id, }); From e6dcc241e0f632f4737cb9e0ea14e80de7f26a8c Mon Sep 17 00:00:00 2001 From: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:06:04 +0530 Subject: [PATCH 27/45] Feature edit git pulled apps/pre release (#13003) * init * ee git-sync * dependancies * added octokit depedencies to server directory * module fixes * fixes * fixes * pull app changes fix * ability factory fixes * code restructuring changes * added gitlab backend changes * app git module fixes * module file changes * added logo images * migration * migration * migration changes * added migration to remove enabledflag from parent table * provider and migration fixes * removed comments * revert appimport export changes * Revert "revert appimport export changes" This reverts commit b139db811e44157eef04ed919c93c315d5837d98. * fixed version rename api calls * app/version rename commit fixes * added builder permissions * review comment changes * module file and service fixes * module fixes * fixes * fixed module file changes * added git-sync repository * fixed app-git imports * removed injected ssh,https, gitlab repositories * added app git sync repository (dev testing pending) * removed more modules * removed type orm completley (dev testing pending) * fixed module file * removed unused dto's * working * fixes * removed comments * migration changes * removed node git package * changed default branch to main * ssh * removed apps ability factory dependencies * minor changes * migration fixes * fixes * added events for app and version rename * removed comments * added license checks * listener fixes * removed unused files * fixed db:reset and server issues * fixed ce and ee migration * submoudle commits * migration and entity changes * module changes * backend changes for edit app functionality * ui changes * added app canvas banner * api-fix for freeze-editor apis * header changes * fix for delete version failing * minor ui fix * minor changes * reverts * reverts * fixes * fixed imports * removed consoles * added separate data migration for migrating provider column values * fix * fixes * fixed ui breaking * fix * added app canvas banner in ce --------- Co-authored-by: Rudra deep Biswas Co-authored-by: rohan Co-authored-by: Midhun G S --- frontend/ee | 2 +- .../src/AppBuilder/AppCanvas/AppCanvas.jsx | 4 +- .../src/AppBuilder/Header/AppCanvasBanner.jsx | 20 ++++++ .../src/AppBuilder/Header/CustomSelect.jsx | 29 +++++--- .../src/AppBuilder/Header/EditAppName.jsx | 1 - .../AppBuilder/_stores/slices/gitSyncSlice.js | 27 +++++-- frontend/src/HomePage/HomePage.jsx | 72 ++++++++++++++----- frontend/src/_services/git_sync.service.js | 6 +- frontend/src/_styles/theme.scss | 34 ++++++++- frontend/src/_ui/Icon/solidIcons/PullIcon.jsx | 19 +++++ frontend/src/_ui/Icon/solidIcons/PushIcon.jsx | 19 +++++ .../src/_ui/Icon/solidIcons/SourceControl.jsx | 15 ++++ frontend/src/_ui/Icon/solidIcons/index.js | 9 +++ .../PromoteVersionButton.jsx | 31 ++++---- .../PromoteConfirmationModal.jsx | 6 +- ...124-MigrateEnabledFlagToProviderColumns.ts | 35 +++++++++ server/ee | 2 +- .../1744630818000-AddEnabledFlagAppGit.ts | 24 +++++++ ...923859030-AddEnabledColumnProviderTable.ts | 32 --------- server/src/entities/app_git_sync.entity.ts | 3 + server/src/modules/app-git/ability/index.ts | 3 +- .../src/modules/app-git/constants/feature.ts | 2 + server/src/modules/app-git/constants/index.ts | 2 + server/src/modules/app-git/controller.ts | 29 +++++++- server/src/modules/app-git/dto/index.ts | 11 +++ server/src/modules/app-git/service.ts | 5 +- server/src/modules/app-git/types/index.ts | 2 + server/src/modules/apps/module.ts | 2 + server/src/modules/apps/service.ts | 10 ++- .../constants/gitsync_error.constant.ts | 2 +- server/src/modules/git-sync/module.ts | 3 + server/src/modules/modules/module.ts | 2 + .../versions/interfaces/IUtilService.ts | 2 +- server/src/modules/versions/module.ts | 2 + server/src/modules/versions/service.ts | 12 ++-- server/src/modules/versions/util.service.ts | 8 +-- server/src/modules/workflows/module.ts | 2 + 37 files changed, 383 insertions(+), 106 deletions(-) create mode 100644 frontend/src/AppBuilder/Header/AppCanvasBanner.jsx create mode 100644 frontend/src/_ui/Icon/solidIcons/PullIcon.jsx create mode 100644 frontend/src/_ui/Icon/solidIcons/PushIcon.jsx create mode 100644 frontend/src/_ui/Icon/solidIcons/SourceControl.jsx create mode 100644 server/data-migrations/1749711866124-MigrateEnabledFlagToProviderColumns.ts create mode 100644 server/migrations/1744630818000-AddEnabledFlagAppGit.ts diff --git a/frontend/ee b/frontend/ee index fcefb9417e..8aaec8a424 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit fcefb9417ea42d07e86236e12c3e6cb5160fe7c6 +Subproject commit 8aaec8a4242f4a24fe7d28768e0c4341f50def82 diff --git a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx index 10ab1fa054..6f8711fe2e 100644 --- a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx +++ b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx @@ -17,6 +17,7 @@ import useAppDarkMode from '@/_hooks/useAppDarkMode'; import useAppCanvasMaxWidth from './useAppCanvasMaxWidth'; import { DeleteWidgetConfirmation } from './DeleteWidgetConfirmation'; import useSidebarMargin from './useSidebarMargin'; +import AppCanvasBanner from '../../AppBuilder/Header/AppCanvasBanner'; export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) => { const { moduleId, isModuleMode, appType } = useModuleContext(); @@ -125,8 +126,7 @@ export const AppCanvas = ({ appId, isViewerSidebarPinned, isViewer = false }) => id="main-editor-canvas" onMouseUp={handleCanvasContainerMouseUp} > - {creationMode === 'GIT' && } - {creationMode !== 'GIT' && } +
{ + const { moduleId } = useModuleContext(); + const currentMode = useStore((state) => state.modeStore.modules[moduleId].currentMode, shallow); + const renderBanner = () => { + if (currentMode === 'edit') { + return ; + } + return null; + }; + return
{renderBanner()}
; +}; + +export default withEditionSpecificComponent(AppCanvasBanner, 'Appbuilder'); diff --git a/frontend/src/AppBuilder/Header/CustomSelect.jsx b/frontend/src/AppBuilder/Header/CustomSelect.jsx index 0ea5199b36..b29e7f4195 100644 --- a/frontend/src/AppBuilder/Header/CustomSelect.jsx +++ b/frontend/src/AppBuilder/Header/CustomSelect.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import cx from 'classnames'; import Select from '@/_ui/Select'; import { components } from 'react-select'; @@ -8,18 +8,27 @@ import { ToolTip } from '@/_components/ToolTip'; import EditWhite from '@assets/images/icons/edit-white.svg'; import { defaultAppEnvironments, decodeEntities } from '@/_helpers/utils'; import { CreateVersionModal } from '@/modules/Appbuilder/components'; +import useStore from '@/AppBuilder/_stores/store'; // TODO: edit version modal and add version modal const Menu = (props) => { const isEditable = props.selectProps.isEditable; + const creationMode = props?.selectProps?.appCreationMode; + const allowAppEdit = useStore((state) => state.allowEditing); + const [isVersionCreationEnabled, setIsVersionCreationEnabled] = useState( + creationMode !== 'GIT' || (creationMode === 'GIT' && allowAppEdit) + ); + useEffect(() => { + setIsVersionCreationEnabled(creationMode !== 'GIT' || (creationMode === 'GIT' && allowAppEdit)); + }, [allowAppEdit, creationMode]); return (
{isEditable && !props?.selectProps?.value?.isReleasedVersion && (
{ style={{ padding: '8px 12px' }} onClick={() => !props?.selectProps?.value?.isReleasedVersion && - props?.selectProps?.appCreationMode !== 'GIT' && + isVersionCreationEnabled && props.selectProps.setShowEditAppVersion(true) } > @@ -49,18 +58,18 @@ const Menu = (props) => {
{props.children}
{isEditable && (
props?.selectProps?.appCreationMode !== 'GIT' && props?.setShowCreateAppVersion(true)} + onClick={() => isVersionCreationEnabled && props?.setShowCreateAppVersion(true)} data-cy="create-new-version-button" > { fillRule="evenodd" clipRule="evenodd" d="M17 11C17.4142 11 17.75 11.3358 17.75 11.75V16.25H22.25C22.6642 16.25 23 16.5858 23 17C23 17.4142 22.6642 17.75 22.25 17.75H17.75V22.25C17.75 22.6642 17.4142 23 17 23C16.5858 23 16.25 22.6642 16.25 22.25V17.75H11.75C11.3358 17.75 11 17.4142 11 17C11 16.5858 11.3358 16.25 11.75 16.25H16.25V11.75C16.25 11.3358 16.5858 11 17 11Z" - fill={`${props?.selectProps?.appCreationMode !== 'GIT' ? '#3E63DD' : '#C1C8CD'}`} + fill={`${isVersionCreationEnabled ? '#3E63DD' : '#C1C8CD'}`} /> Create new version diff --git a/frontend/src/AppBuilder/Header/EditAppName.jsx b/frontend/src/AppBuilder/Header/EditAppName.jsx index e9dff6074a..24b6dbbf61 100644 --- a/frontend/src/AppBuilder/Header/EditAppName.jsx +++ b/frontend/src/AppBuilder/Header/EditAppName.jsx @@ -149,7 +149,6 @@ function EditAppName() { value={name} maxLength={50} data-cy="app-name-input" - disabled={appCreationMode === 'GIT'} /> ({ ...initialState, - toggleGitSyncModal: () => { - const creationMode = useStore.getState()?.app.creationMode; + toggleGitSyncModal: (creationMode) => { const featureAccess = useStore.getState()?.license?.featureAccess; const selectedEnvironment = useStore.getState()?.selectedEnvironment; const isEditorFreezed = useStore.getState()?.isEditorFreezed; @@ -16,4 +17,22 @@ export const createGitSyncSlice = (set, get) => ({ ? set((state) => ({ showGitSyncModal: !state.showGitSyncModal }), false, 'toggleGitSyncModal') : () => {}; }, + fetchAppGit: async (currentOrganizationId, currentAppVersionId) => { + set((state) => ({ appLoading: true }), false, 'setAppLoading'); + + try { + const data = await gitSyncService.getAppGitConfigs(currentOrganizationId, currentAppVersionId); + const allowEditing = data?.app_git?.allow_editing ?? false; + + set((state) => ({ allowEditing }), false, 'setAllowEditing'); + return allowEditing; + } catch (error) { + console.error('Failed to fetch app git configs:', error); + // Set allowEditing to false on error + set((state) => ({ allowEditing: false }), false, 'setAllowEditing'); + return false; + } finally { + set((state) => ({ appLoading: false }), false, 'setAppLoading'); + } + }, }); diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index b0d698398e..deeada63a4 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -118,6 +118,8 @@ class HomePageComponent extends React.Component { shouldAutoImportPlugin: false, dependentPlugins: [], dependentPluginsDetail: {}, + importedAppName: {}, + isAppImportEditable: false, showMissingGroupsModal: false, missingGroups: [], missingGroupsExpanded: false, @@ -655,6 +657,8 @@ class HomePageComponent extends React.Component { lastCommitUser: last_commit_user, lastPushDate: new Date(lastpush_date), organizationGitId: orgGit?.id, + appName: this.state.importedAppName, + allowEditing: this.state.isAppImportEditable, }; gitSyncService .importGitApp(body) @@ -912,6 +916,25 @@ class HomePageComponent extends React.Component { appType !== 'workflow' ); }; + handleAppNameChange = (e) => { + const newAppName = e.target.value; + const { appsFromRepos } = this.state; + let validationMessage = {}; + if (!newAppName.trim()) { + validationMessage = { message: 'App name cannot be empty' }; + } else if (newAppName.length > 50) { + validationMessage = { message: 'App name cannot exceed 50 characters' }; + } else { + const matchingApp = Object.values(appsFromRepos).find((app) => app.git_app_name === newAppName.trim()); + if (matchingApp?.app_name_exist === 'EXIST') { + validationMessage = { message: 'App name already exists' }; + } + } + this.setState({ + importedAppName: newAppName, + importingGitAppOperations: validationMessage, + }); + }; render() { const { apps, @@ -1192,13 +1215,16 @@ class HomePageComponent extends React.Component { options={this.generateOptionsForRepository()} disabled={importingApp} onChange={(newVal) => { - this.setState({ selectedAppRepo: newVal }, () => { - if (appsFromRepos[newVal]?.app_name_exist === 'EXIST') { - this.setState({ importingGitAppOperations: { message: 'App name already exists' } }); - } else { - this.setState({ importingGitAppOperations: {} }); + this.setState( + { selectedAppRepo: newVal, importedAppName: appsFromRepos[newVal]?.git_app_name }, + () => { + if (appsFromRepos[newVal]?.app_name_exist === 'EXIST') { + this.setState({ importingGitAppOperations: { message: 'App name already exists' } }); + } else { + this.setState({ importingGitAppOperations: {} }); + } } - }); + ); }} width={'100%'} value={selectedAppRepo} @@ -1217,12 +1243,11 @@ class HomePageComponent extends React.Component {
@@ -1233,9 +1258,24 @@ class HomePageComponent extends React.Component { )} data-cy="app-name-helper-text" > - {importingGitAppOperations?.message - ? importingGitAppOperations?.message - : 'App name is inherited from git repository and cannot be edited'} + {importingGitAppOperations?.message} +
+
+
+
+ + this.setState((prevState) => ({ isAppImportEditable: !prevState.isAppImportEditable })) + } + /> + Make application editable +
+
+
+ Enabling this allows editing and git sync push/pull access in development.
@@ -1464,12 +1504,6 @@ class HomePageComponent extends React.Component { classes={`${this.props.darkMode ? 'theme-dark dark-theme m-3 trial-banner' : 'm-3 trial-banner'}`} /> )} - {this.shouldShowMigrationBanner() && ( - - )}
diff --git a/frontend/src/_services/git_sync.service.js b/frontend/src/_services/git_sync.service.js index a710691872..25df504e10 100644 --- a/frontend/src/_services/git_sync.service.js +++ b/frontend/src/_services/git_sync.service.js @@ -210,7 +210,7 @@ function updateAppEditState(appId, allowEditing) { credentials: 'include', body: JSON.stringify(body), }; - return fetch(`${config.apiUrl}/git-sync/appGit/${appId}`, requestOptions).then(handleResponse); + return fetch(`${config.apiUrl}/app-git/${appId}/configs`, requestOptions).then(handleResponse); } function getAppGitConfigs(workspaceId, versionId) { const requestOptions = { @@ -219,8 +219,6 @@ function getAppGitConfigs(workspaceId, versionId) { credentials: 'include', }; - return fetch(`${config.apiUrl}/git-sync/${workspaceId}/app/${versionId}/configs`, requestOptions).then( - handleResponse - ); + return fetch(`${config.apiUrl}/app-git/${workspaceId}/app/${versionId}/configs`, requestOptions).then(handleResponse); } // Remove all app-git api's to separate service from here. diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index fefe527bf7..858927c492 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -15356,7 +15356,9 @@ tbody { .create-commit-container, .commit-info, - .pull-container { + .pull-container, + .pushpull-container { + height: 260px !important; .form-control { font-weight: 400; font-size: 12px; @@ -19016,3 +19018,33 @@ section.ai-message-prompt-input-wrapper { } } } + + +.application-editable-checkbox-container { + .form-check-input { + margin-right: 9px !important; + margin-bottom: 5px !important; + font-size: 12px !important; + } + + .helper-text { + margin-left: 25px !important; + margin-bottom: 10px !important; + } +} + +.git-sync-modal { + width: 400px !important; + height: 484px !important; +} + +.dark-theme.git-sync-modal { + .modal-header { + border-bottom: 1px solid var(--slate5) !important; + } +} + +.git-sync-modal .modal-header .modal-title .push-pull-tabs .tab-push.active, +.git-sync-modal .modal-header .modal-title .push-pull-tabs .tab-pull.active { + border-bottom: 2px solid var(--indigo9) !important; +} \ No newline at end of file diff --git a/frontend/src/_ui/Icon/solidIcons/PullIcon.jsx b/frontend/src/_ui/Icon/solidIcons/PullIcon.jsx new file mode 100644 index 0000000000..5c1520e686 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/PullIcon.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const Pull = ({ fill = '#ACB2B9', width = '14', height = '18', viewBox = '0 0 14 18', className = '' }) => ( + + + + + + + + + + +); + +export default Pull; diff --git a/frontend/src/_ui/Icon/solidIcons/PushIcon.jsx b/frontend/src/_ui/Icon/solidIcons/PushIcon.jsx new file mode 100644 index 0000000000..14f2797e8e --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/PushIcon.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const Push = ({ fill = '#4368E3', width = '14', height = '18', viewBox = '0 0 14 18', className = '' }) => ( + + + + + + + + + + +); + +export default Push; diff --git a/frontend/src/_ui/Icon/solidIcons/SourceControl.jsx b/frontend/src/_ui/Icon/solidIcons/SourceControl.jsx new file mode 100644 index 0000000000..563781a7dc --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/SourceControl.jsx @@ -0,0 +1,15 @@ +import React from 'react'; + +const SourceControl = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => ( + + + + +); + +export default SourceControl; diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js index 0e7192edaf..3ca0ee8801 100644 --- a/frontend/src/_ui/Icon/solidIcons/index.js +++ b/frontend/src/_ui/Icon/solidIcons/index.js @@ -239,6 +239,9 @@ import EnterpriseCrown from './EnterrpiseCrown.jsx'; import FileCode from './FileCode.jsx'; import Corners from './Corners.jsx'; import Moon from './Moon.jsx'; +import SourceControl from './SourceControl.jsx'; +import Push from './PushIcon.jsx'; +import Pull from './PullIcon.jsx'; import RemoveFolder from './RemoveFolder.jsx'; const Icon = (props) => { @@ -725,6 +728,12 @@ const Icon = (props) => { return ; case 'moon': return ; + case 'source-control': + return ; + case 'push-changes': + return ; + case 'pull-changes': + return ; default: return ; } diff --git a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx index f30c40b82f..ef0279712a 100644 --- a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx +++ b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx @@ -9,20 +9,25 @@ import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; const PromoteVersionButton = () => { const { moduleId } = useModuleContext(); const [promoteModalData, setPromoteModalData] = useState(null); - const { isSaving, editingVersion, appVersionEnvironment, environments, selectedEnvironment, currentEnvIndex } = useStore( - (state) => ({ - isSaving: state.appStore.modules[moduleId].app.isSaving, - editingVersion: state.currentVersionId, - selectedEnvironment: state.selectedEnvironment, - environments: state.environments, - appVersionEnvironment: state.appVersionEnvironment, - currentEnvIndex: state.environments?.findIndex((env) => env?.id === state.appVersionEnvironment?.id), - }), - shallow - ); + const { isSaving, editingVersion, appVersionEnvironment, environments, selectedEnvironment, currentEnvIndex } = + useStore( + (state) => ({ + isSaving: state.appStore.modules[moduleId].app.isSaving, + editingVersion: state.currentVersionId, + selectedEnvironment: state.selectedEnvironment, + environments: state.environments, + appVersionEnvironment: state.appVersionEnvironment, + currentEnvIndex: state.environments?.findIndex((env) => env?.id === state.appVersionEnvironment?.id), + }), + shallow + ); // enable only after the environment details are loaded - const shouldDisablePromote = isSaving || selectedEnvironment?.priority < appVersionEnvironment?.priority || !appVersionEnvironment || !environments?.[currentEnvIndex + 1]; + const shouldDisablePromote = + isSaving || + selectedEnvironment?.priority < appVersionEnvironment?.priority || + !appVersionEnvironment || + !environments?.[currentEnvIndex + 1]; const handlePromote = () => { setPromoteModalData({ @@ -57,7 +62,7 @@ const PromoteVersionButton = () => { data={promoteModalData} editingVersion={editingVersion} onClose={() => setPromoteModalData(null)} - fetchEnvironments={() => { }} + fetchEnvironments={() => {}} /> ); diff --git a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx index cbc1284c06..7c8be912d5 100644 --- a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx +++ b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx @@ -38,6 +38,7 @@ const PromoteConfirmationModal = React.memo(({ data, onClose }) => { onClose(); setShow(false); }, [promotingEnvironment, onClose]); + const allowAppEdit = useStore((state) => state.allowEditing); const handleConfirm = () => { setPromotingEnvironment(true); @@ -46,7 +47,10 @@ const PromoteConfirmationModal = React.memo(({ data, onClose }) => { currentVersionId, async (response) => { toast.success(`${selectedVersion.name} has been promoted to ${data.target.name}!`); - if (data?.current?.name == 'development' && creationMode !== 'GIT') { + if ( + data?.current?.name == 'development' && + (creationMode !== 'GIT' || (creationMode === 'GIT' && allowAppEdit)) + ) { try { const gitData = await gitSyncService.getAppConfig(current_organization_id, selectedVersion?.id); const appGit = gitData?.app_git; diff --git a/server/data-migrations/1749711866124-MigrateEnabledFlagToProviderColumns.ts b/server/data-migrations/1749711866124-MigrateEnabledFlagToProviderColumns.ts new file mode 100644 index 0000000000..1ec386133b --- /dev/null +++ b/server/data-migrations/1749711866124-MigrateEnabledFlagToProviderColumns.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MigrateEnabledFlagToProviderColumns1749711866124 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE "organization_git_ssh" git_ssh + SET "is_enabled" = TRUE + FROM "organization_git_sync" git_sync + WHERE git_ssh."config_id" = git_sync."id" + AND git_sync."is_enabled" = TRUE + AND git_sync."git_type" = 'github_ssh'; + `); + + await queryRunner.query(` + UPDATE "organization_git_https" git_https + SET "is_enabled" = TRUE + FROM "organization_git_sync" git_sync + WHERE git_https."config_id" = git_sync."id" + AND git_sync."is_enabled" = TRUE + AND git_sync."git_type" = 'github_https'; + `); + await queryRunner.query(` + UPDATE "organization_gitlab" gitlab + SET "is_enabled" = TRUE + FROM "organization_git_sync" git_sync + WHERE gitlab."config_id" = git_sync."id" + AND git_sync."is_enabled" = TRUE + AND git_sync."git_type" = 'gitlab'; + `); + await queryRunner.dropColumn('organization_git_sync', 'is_enabled'); + await queryRunner.dropColumn('organization_git_sync', 'git_type'); + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/server/ee b/server/ee index aff18e4808..d20489cb3a 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit aff18e48080fc533173d49b68e7ac397b46ae5b1 +Subproject commit d20489cb3a5e62d4cd676d77e95edccdb719e02f diff --git a/server/migrations/1744630818000-AddEnabledFlagAppGit.ts b/server/migrations/1744630818000-AddEnabledFlagAppGit.ts new file mode 100644 index 0000000000..8ea2e40220 --- /dev/null +++ b/server/migrations/1744630818000-AddEnabledFlagAppGit.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddEnabledFlagAppGit1744630818000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + 'app_git_sync', + new TableColumn({ + name: 'allow_editing', + type: 'boolean', + isNullable: false, + default: false, + }) + ); + await queryRunner.query(` + UPDATE app_git_sync + SET allow_editing = true + WHERE app_id IN ( + SELECT id FROM apps WHERE creation_mode = 'DEFAULT' + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/server/migrations/1747923859030-AddEnabledColumnProviderTable.ts b/server/migrations/1747923859030-AddEnabledColumnProviderTable.ts index f4d82d7011..d26ce3169a 100644 --- a/server/migrations/1747923859030-AddEnabledColumnProviderTable.ts +++ b/server/migrations/1747923859030-AddEnabledColumnProviderTable.ts @@ -13,38 +13,6 @@ export class AddEnabledColumnProviderTable1747923859030 implements MigrationInte await queryRunner.query(` ALTER TABLE "organization_gitlab" ADD COLUMN IF NOT EXISTS "is_enabled" boolean DEFAULT FALSE; `); - - await queryRunner.query(` - UPDATE "organization_git_ssh" git_ssh - SET "is_enabled" = TRUE - FROM "organization_git_sync" git_sync - WHERE git_ssh."config_id" = git_sync."id" - AND git_sync."is_enabled" = TRUE - AND git_sync."git_type" = 'github_ssh'; - `); - - await queryRunner.query(` - UPDATE "organization_git_https" git_https - SET "is_enabled" = TRUE - FROM "organization_git_sync" git_sync - WHERE git_https."config_id" = git_sync."id" - AND git_sync."is_enabled" = TRUE - AND git_sync."git_type" = 'github_https'; - `); - - await queryRunner.commitTransaction(); - await queryRunner.startTransaction(); - - await queryRunner.query(` - UPDATE "organization_gitlab" gitlab - SET "is_enabled" = TRUE - FROM "organization_git_sync" git_sync - WHERE gitlab."config_id" = git_sync."id" - AND git_sync."is_enabled" = TRUE - AND git_sync."git_type" = 'gitlab'; - `); - await queryRunner.dropColumn('organization_git_sync', 'is_enabled'); - await queryRunner.dropColumn('organization_git_sync', 'git_type'); } public async down(queryRunner: QueryRunner): Promise {} diff --git a/server/src/entities/app_git_sync.entity.ts b/server/src/entities/app_git_sync.entity.ts index 3e7536fcd4..b5a4fcd931 100644 --- a/server/src/entities/app_git_sync.entity.ts +++ b/server/src/entities/app_git_sync.entity.ts @@ -61,6 +61,9 @@ export class AppGitSync extends BaseEntity { @JoinColumn({ name: 'app_id' }) app: App; + @Column({ name: 'allow_editing', default: false, nullable: false }) + allowEditing: boolean; + @CreateDateColumn({ default: () => 'now()', name: 'created_at' }) createdAt: Date; diff --git a/server/src/modules/app-git/ability/index.ts b/server/src/modules/app-git/ability/index.ts index 539e252911..8996f754b7 100644 --- a/server/src/modules/app-git/ability/index.ts +++ b/server/src/modules/app-git/ability/index.ts @@ -27,7 +27,6 @@ export class AppGitAbilityFactory extends AbilityFactory const isAllAppsEditable = !!userAppGitPermissions?.isAllEditable; const isAllAppsCreatable = !!userPermission?.appCreate; const isAllAppsViewable = !!userAppGitPermissions?.isAllViewable; - // Grant feature-level access based on resource actions if (isAdmin || superAdmin || isBuilder) { // Admin or Super Admin gets full access to all features @@ -38,6 +37,8 @@ export class AppGitAbilityFactory extends AbilityFactory can(FEATURE_KEY.GIT_GET_APP_CONFIG, App); can(FEATURE_KEY.GIT_SYNC_APP, App); can(FEATURE_KEY.GIT_APP_VERSION_RENAME, App); + can(FEATURE_KEY.GIT_APP_CONFIGS_UPDATE, App); + can(FEATURE_KEY.GIT_FETCH_APP_CONFIGS, App); return; } diff --git a/server/src/modules/app-git/constants/feature.ts b/server/src/modules/app-git/constants/feature.ts index b875af3bcd..5c5cada809 100644 --- a/server/src/modules/app-git/constants/feature.ts +++ b/server/src/modules/app-git/constants/feature.ts @@ -12,5 +12,7 @@ export const FEATURES: FeaturesConfig = { [FEATURE_KEY.GIT_SYNC_APP]: { license: LICENSE_FIELD.VALID }, [FEATURE_KEY.GIT_UPDATE_APP]: { license: LICENSE_FIELD.VALID }, [FEATURE_KEY.GIT_APP_VERSION_RENAME]: { license: LICENSE_FIELD.VALID }, + [FEATURE_KEY.GIT_APP_CONFIGS_UPDATE]: { license: LICENSE_FIELD.VALID }, + [FEATURE_KEY.GIT_FETCH_APP_CONFIGS]: { isPublic: true }, }, }; diff --git a/server/src/modules/app-git/constants/index.ts b/server/src/modules/app-git/constants/index.ts index 7955e51730..51cebdc13f 100644 --- a/server/src/modules/app-git/constants/index.ts +++ b/server/src/modules/app-git/constants/index.ts @@ -6,4 +6,6 @@ export enum FEATURE_KEY { GIT_GET_APP_CONFIG = 'git_get_app_config', // Corresponds to getAppConfig (GET ':workspaceId/app/:versionId') GIT_SYNC_APP = 'git_sync_app', // Corresponds to gitSyncApp (POST 'gitpush/:appGitId/:versionId') GIT_APP_VERSION_RENAME = 'git_app_version_rename', // Corresponds to gitSyncApp (POST 'gitpush/:appGitId/:versionId') + GIT_APP_CONFIGS_UPDATE = 'git_app_configs_update', + GIT_FETCH_APP_CONFIGS = 'get_app_git_configs', } diff --git a/server/src/modules/app-git/controller.ts b/server/src/modules/app-git/controller.ts index f7a7d12b03..df68e38551 100644 --- a/server/src/modules/app-git/controller.ts +++ b/server/src/modules/app-git/controller.ts @@ -1,7 +1,13 @@ import { Controller, Get, UseGuards, Post, Param, Body, NotFoundException, Put } from '@nestjs/common'; import { JwtAuthGuard } from '../session/guards/jwt-auth.guard'; import { User, UserEntity } from '@modules/app/decorators/user.decorator'; -import { AppGitPullDto, AppGitPullUpdateDto, AppGitPushDto, RenameAppOrVersionDto } from '@modules/app-git/dto'; +import { + AppGitPullDto, + AppGitPullUpdateDto, + AppGitPushDto, + AppGitUpdateDto, + RenameAppOrVersionDto, +} from '@modules/app-git/dto'; import { MODULES } from '@modules/app/constants/modules'; import { InitModule } from '@modules/app/decorators/init-module'; import { LICENSE_FIELD } from '@modules/licensing/constants'; @@ -80,4 +86,25 @@ export class AppGitController { ) { throw new NotFoundException(); } + + @RequireFeature(LICENSE_FIELD.GIT_SYNC) + @InitFeature(FEATURE_KEY.GIT_APP_CONFIGS_UPDATE) + @Put(':appId/configs') + async updateAppGitConfigs( + @User() user: UserEntity, + @Param('appId') appId: string, + @Body() updateAppGitDto: AppGitUpdateDto + ) { + throw new NotFoundException(); + } + + @RequireFeature(LICENSE_FIELD.GIT_SYNC) + @Get(':workspaceId/app/:versionId/configs') + async getAppGitConfigs( + @User() user, + @Param('workspaceId') organizationId: string, + @Param('versionId') versionId: string + ) { + throw new NotFoundException(); + } } diff --git a/server/src/modules/app-git/dto/index.ts b/server/src/modules/app-git/dto/index.ts index a3e39c490c..55cdd31a49 100644 --- a/server/src/modules/app-git/dto/index.ts +++ b/server/src/modules/app-git/dto/index.ts @@ -16,6 +16,10 @@ export class AppGitCreateDto { @IsString() @IsNotEmpty() gitAppName: string; + + @IsBoolean() + @IsOptional() + allowEditing: boolean; } export class AppGitPushDto { @@ -56,6 +60,13 @@ export class AppGitPullDto { @IsString() gitVersionName: string; + + @IsString() + appName: string; + + @IsBoolean() + @IsOptional() + allowEditing: boolean; } export class AppGitPullUpdateDto { diff --git a/server/src/modules/app-git/service.ts b/server/src/modules/app-git/service.ts index 6ec1672bee..76209e6131 100644 --- a/server/src/modules/app-git/service.ts +++ b/server/src/modules/app-git/service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { AppGitPullDto, AppGitPullUpdateDto, AppGitPushDto } from '@modules/app-git/dto'; +import { AppGitPullDto, AppGitPullUpdateDto, AppGitPushDto, AppGitUpdateDto } from '@modules/app-git/dto'; import { User } from 'src/entities/user.entity'; @Injectable() @@ -23,4 +23,7 @@ export class AppGitService { async pullGitAppChanges(user: User, appMetaBody: AppGitPullUpdateDto, appId: string): Promise { throw new Error('Method not implemented.'); } + async updateAppGitConfiguration(user: User, appId: string, updateAppGitDto: AppGitUpdateDto): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/server/src/modules/app-git/types/index.ts b/server/src/modules/app-git/types/index.ts index fe4bccf048..c4a718c480 100644 --- a/server/src/modules/app-git/types/index.ts +++ b/server/src/modules/app-git/types/index.ts @@ -10,6 +10,8 @@ interface Features { [FEATURE_KEY.GIT_SYNC_APP]: FeatureConfig; [FEATURE_KEY.GIT_UPDATE_APP]: FeatureConfig; [FEATURE_KEY.GIT_APP_VERSION_RENAME]: FeatureConfig; + [FEATURE_KEY.GIT_APP_CONFIGS_UPDATE]: FeatureConfig; + [FEATURE_KEY.GIT_FETCH_APP_CONFIGS]: FeatureConfig; } export interface FeaturesConfig { diff --git a/server/src/modules/apps/module.ts b/server/src/modules/apps/module.ts index 2a54c29326..6c9d65b2d6 100644 --- a/server/src/modules/apps/module.ts +++ b/server/src/modules/apps/module.ts @@ -22,6 +22,7 @@ import { AiModule } from '@modules/ai/module'; import { AppPermissionsModule } from '@modules/app-permissions/module'; import { RolesRepository } from '@modules/roles/repository'; import { UsersModule } from '@modules/users/module'; +import { AppGitRepository } from '@modules/app-git/repository'; @Module({}) export class AppsModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -55,6 +56,7 @@ export class AppsModule { AppsService, VersionRepository, AppsRepository, + AppGitRepository, AppEnvironmentUtilService, PageService, EventsService, diff --git a/server/src/modules/apps/service.ts b/server/src/modules/apps/service.ts index 66c0c681bb..f792da7af7 100644 --- a/server/src/modules/apps/service.ts +++ b/server/src/modules/apps/service.ts @@ -41,6 +41,7 @@ import { RequestContext } from '@modules/request-context/service'; import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants'; import { MODULES } from '@modules/app/constants/modules'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { AppGitRepository } from '@modules/app-git/repository'; @Injectable() export class AppsService implements IAppsService { @@ -57,7 +58,8 @@ export class AppsService implements IAppsService { protected readonly organizationThemeUtilService: OrganizationThemesUtilService, protected readonly aiUtilService: AiUtilService, protected readonly componentsService: ComponentsService, - protected readonly eventEmitter: EventEmitter2 + protected readonly eventEmitter: EventEmitter2, + protected readonly appGitRepository: AppGitRepository ) { } async create(user: User, appCreateDto: AppCreateDto) { const { name, icon, type } = appCreateDto; @@ -295,7 +297,11 @@ export class AppsService implements IAppsService { appVersionEnvironment = await this.appEnvironmentUtilService.getByPriority(user.organizationId); response['editing_version']['current_environment_id'] = appVersionEnvironment.id; } - response['should_freeze_editor'] = app.creationMode === 'GIT' || shouldFreezeEditor; + response['should_freeze_editor'] = shouldFreezeEditor; + const appGit = await this.appGitRepository.findAppGitByAppId(app.id); + if (appGit) { + response['should_freeze_editor'] = !appGit.allowEditing || shouldFreezeEditor; + } response['editorEnvironment'] = { id: appVersionEnvironment.id, name: appVersionEnvironment.name, diff --git a/server/src/modules/git-sync/constants/gitsync_error.constant.ts b/server/src/modules/git-sync/constants/gitsync_error.constant.ts index 49671c03dc..d7e810bf41 100644 --- a/server/src/modules/git-sync/constants/gitsync_error.constant.ts +++ b/server/src/modules/git-sync/constants/gitsync_error.constant.ts @@ -1,7 +1,7 @@ export enum GitErrorMessages { BRANCH_NOT_FOUND = 'Specified branch from env variable is \n missing in Github repository', BRANCH_NAME_MISMATCH = 'Could not push commit to the repository. Please ensure your default branch name is master and try again.', - GENERIC_CLONE_ERROR = 'Issue while cloning', + GENERIC_CLONE_ERROR = 'Issue while cloning. Please try again.', REPOSITORY_NOT_FOUND = 'Repository not found. Please verify the repository URL is correct and accessible.', INVALID_PRIVATE_KEY = "Invalid GitHub private key format. Please check the key and ensure it's properly formatted.", INVALID_APP_ID = 'Invalid GitHub App ID. Please verify the App ID and try again.', diff --git a/server/src/modules/git-sync/module.ts b/server/src/modules/git-sync/module.ts index 3eb8af3970..e11bd129b9 100644 --- a/server/src/modules/git-sync/module.ts +++ b/server/src/modules/git-sync/module.ts @@ -6,6 +6,8 @@ import { AppsModule } from '@modules/apps/module'; import { VersionModule } from '@modules/versions/module'; import { OrganizationGitSyncRepository } from './repository'; import { VersionRepository } from '@modules/versions/repository'; +import { AppGitRepository } from '@modules/app-git/repository'; + export class GitSyncModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { const { GitSyncController } = await import(`${await getImportPath(configs?.IS_GET_CONTEXT)}/git-sync/controller`); @@ -49,6 +51,7 @@ export class GitSyncModule { providers: [ OrganizationGitSyncRepository, VersionRepository, + AppGitRepository, BaseGitUtilService, BaseGitSyncService, GitSyncService, diff --git a/server/src/modules/modules/module.ts b/server/src/modules/modules/module.ts index dc442ffa8c..59c545174e 100644 --- a/server/src/modules/modules/module.ts +++ b/server/src/modules/modules/module.ts @@ -13,6 +13,7 @@ import { AiModule } from '@modules/ai/module'; import { AppsRepository } from '@modules/apps/repository'; import { AppPermissionsModule } from '@modules/app-permissions/module'; import { RolesRepository } from '@modules/roles/repository'; +import { AppGitRepository } from '@modules/app-git/repository'; @Module({}) export class ModulesModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -50,6 +51,7 @@ export class ModulesModule { OrganizationRepository, DataSourcesRepository, RolesRepository, + AppGitRepository ], }; } diff --git a/server/src/modules/versions/interfaces/IUtilService.ts b/server/src/modules/versions/interfaces/IUtilService.ts index 340b918b03..f68118978e 100644 --- a/server/src/modules/versions/interfaces/IUtilService.ts +++ b/server/src/modules/versions/interfaces/IUtilService.ts @@ -6,6 +6,6 @@ import { EntityManager } from 'typeorm'; export interface IVersionUtilService { updateVersion(appVersion: AppVersion, appVersionUpdateDto: AppVersionUpdateDto): Promise; - deleteVersionGit(app: App, user: User, manager?: EntityManager): Promise; + deleteVersionGit(app: App, version: AppVersion, manager?: EntityManager): Promise; fetchVersions(appId: string): Promise; } diff --git a/server/src/modules/versions/module.ts b/server/src/modules/versions/module.ts index 7fac84f033..894c27206a 100644 --- a/server/src/modules/versions/module.ts +++ b/server/src/modules/versions/module.ts @@ -7,6 +7,7 @@ import { DataQueryRepository } from '@modules/data-queries/repository'; import { DataSourcesRepository } from '@modules/data-sources/repository'; import { DataSourcesModule } from '@modules/data-sources/module'; import { AppsRepository } from '@modules/apps/repository'; +import { AppGitRepository } from '@modules/app-git/repository'; import { FeatureAbilityFactory } from './ability'; import { getImportPath } from '@modules/app/constants'; import { AppPermissionsModule } from '@modules/app-permissions/module'; @@ -46,6 +47,7 @@ export class VersionModule { DataSourcesRepository, VersionRepository, AppsRepository, + AppGitRepository, VersionsCreateService, PageService, EventsService, diff --git a/server/src/modules/versions/service.ts b/server/src/modules/versions/service.ts index 2f936419cd..a71189ad8a 100644 --- a/server/src/modules/versions/service.ts +++ b/server/src/modules/versions/service.ts @@ -22,6 +22,7 @@ import { IVersionService } from './interfaces/IService'; import { RequestContext } from '@modules/request-context/service'; import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { AppGitRepository } from '@modules/app-git/repository'; @Injectable() export class VersionService implements IVersionService { @@ -35,7 +36,8 @@ export class VersionService implements IVersionService { protected readonly licenseTermsService: LicenseTermsService, protected readonly organizationThemesUtilService: OrganizationThemesUtilService, protected readonly versionsUtilService: VersionUtilService, - protected readonly eventEmitter: EventEmitter2 + protected readonly eventEmitter: EventEmitter2, + protected readonly appGitRepository: AppGitRepository ) {} async getAllVersions(app: App): Promise<{ versions: Array }> { const result = await this.versionRepository.getVersionsInApp(app.id); @@ -149,15 +151,17 @@ export class VersionService implements IVersionService { user.organizationId, editingVersion?.globalSettings?.theme?.id ); - + const appGit = await this.appGitRepository.findAppGitByAppId(app.id); + if (appGit) { + shouldFreezeEditor = !appGit.allowEditing || shouldFreezeEditor; + } editingVersion['globalSettings']['theme'] = appTheme; - return { ...appData, editing_version: editingVersion, pages: this.appUtilService.mergeDefaultComponentData(pagesForVersion), events: eventsForVersion, - should_freeze_editor: app.creationMode === 'GIT' || shouldFreezeEditor, + should_freeze_editor: shouldFreezeEditor, }; }; diff --git a/server/src/modules/versions/util.service.ts b/server/src/modules/versions/util.service.ts index 8c1ea9b853..d0400af882 100644 --- a/server/src/modules/versions/util.service.ts +++ b/server/src/modules/versions/util.service.ts @@ -7,9 +7,6 @@ import { dbTransactionWrap } from '@helpers/database.helper'; import { EntityManager } from 'typeorm'; import { App } from '@entities/app.entity'; import { User } from '@entities/user.entity'; -import { RenameAppOrVersionDto } from '@modules/app-git/dto'; -import { RequestContext } from '@modules/request-context/service'; -import got from 'got'; @Injectable() export class VersionUtilService implements IVersionUtilService { @@ -107,13 +104,12 @@ export class VersionUtilService implements IVersionUtilService { return; }, manager); } - async deleteVersionGit(app: App, user: User, manager?: EntityManager): Promise { + async deleteVersionGit(app: App, version: AppVersion, manager?: EntityManager): Promise { return await dbTransactionWrap(async (manager: EntityManager) => { if (app.currentVersionId && app.currentVersionId === app.appVersions[0].id) { throw new BadRequestException('You cannot delete a released version'); } - - await this.versionRepository.deleteById(app.appVersions[0].id, manager); + await this.versionRepository.deleteById(version.id, manager); // TODO: Add audit logs return; diff --git a/server/src/modules/workflows/module.ts b/server/src/modules/workflows/module.ts index 389a754701..9394c36819 100644 --- a/server/src/modules/workflows/module.ts +++ b/server/src/modules/workflows/module.ts @@ -31,6 +31,7 @@ import { AiModule } from '@modules/ai/module'; import { DataSourcesRepository } from '@modules/data-sources/repository'; import { AppPermissionsModule } from '@modules/app-permissions/module'; import { RolesRepository } from '@modules/roles/repository'; +import { AppGitRepository } from '@modules/app-git/repository'; export class WorkflowsModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { const importPath = await getImportPath(configs?.IS_GET_CONTEXT); @@ -104,6 +105,7 @@ export class WorkflowsModule { DataSourcesRepository, OrganizationConstantRepository, VersionRepository, + AppGitRepository, AppsService, PageService, EventsService, From 1d1c7858cd8a44177d736934ec1a412aafb77ff5 Mon Sep 17 00:00:00 2001 From: Devanshu Gupta <86366994+devanshu-gupta2002@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:40:07 +0530 Subject: [PATCH 28/45] Feat/audit logs 2.0 (#13012) * feat: added default audit logs for logout, archive, unarchive, profile_update * feat: added resource data in audit table * feat: added archive workspace, invite redeem * feat: updated profile update audit * feat: updated user invite audit * feat: completed user actions audit * feat: added default audit logs for logout, archive, unarchive, profile_update * feat: added resource data in audit table * feat: added archive workspace, invite redeem * feat: updated profile update audit * feat: updated user invite audit * feat: completed user actions audit * fix: merge conflict * fix: ee commit * fix: removed logs * feat: added migration for resource_data * fix: updated action names * frontend ee commit * feat: added /user/instance route * fix: user instance update * fix: updated feature name * user ee commit * feat: added instance level archive * fix: user details update instance * feat: added self signup audit * ee audit commit * ee commit * metadata workspace field * fix: instace user unarchive data * feat: added grou permission default properties * fix: review fixes * feat: granular app * fix: moved user name logic to service * remove log * ee commit * feat: separate routes for app, data-source * feat: added some actions * ee commit * ee commit * feat: added audits * ee commit * fix pasword * Update the new granular permission api end points in cypress test cases * feat: batch-3 actions * removed log * remove import * feat: added app audit logs * feat: public app update route * feat: added resource route * fix: json clone * feat: added feature audit * revert INSTANCE_UPDATE * feature key update * fix: filter resource guard * ee commit * fe ee commit * script for resolving submodule conflicts * changing docs url to .ai --------- Co-authored-by: ajith-k-v Co-authored-by: Midhun G S Co-authored-by: Rudra deep Biswas --- cypress-tests/cypress/commands/apiCommands.js | 30 +- .../apps/privateAndpublicApps.cy.js | 615 +++++++++--------- .../cypress/support/utils/manageGroups.js | 22 +- frontend/ee | 2 +- .../Components/DataSourcePicker.jsx | 2 +- .../Components/QueryManagerBody.jsx | 6 +- .../Components/Transformation.jsx | 2 +- .../src/AppBuilder/QueryPanel/QueryCard.jsx | 2 +- .../RightSideBar/Inspector/Inspector.jsx | 8 +- frontend/src/Editor/Inspector/Inspector.jsx | 8 +- .../Components/DataSourcePicker.jsx | 2 +- .../Components/QueryManagerBody.jsx | 6 +- .../Components/Transformation.jsx | 2 +- frontend/src/Editor/QueryPanel/QueryCard.jsx | 2 +- .../UserGroupMigrationModal.jsx | 2 +- .../EnableAutomaticSSOLoginModal.jsx | 2 +- .../NotificationBanner/NotificationBanner.jsx | 2 +- frontend/src/_services/apps.service.js | 2 +- frontend/src/_services/auditLogsService.js | 7 +- .../_services/groupPermission.v2.service.js | 21 +- .../src/_services/organization.service.js | 32 +- .../src/_ui/Drawer/DrawerFooter/index.jsx | 10 +- .../BaseSSOConfigurationList.jsx | 2 +- .../BaseManageGranularAccess.jsx | 9 +- .../BaseManageGroupPermissionResources.jsx | 2 +- .../BaseManageOrgConstants.jsx | 2 +- .../DataSourceManager/DataSourceManager.jsx | 10 +- resolve-conflicts-submodules.sh | 69 ++ server/ee | 2 +- .../src/modules/apps/ability/app.ability.ts | 2 + server/src/modules/apps/constants/features.ts | 5 +- server/src/modules/apps/constants/index.ts | 1 + server/src/modules/apps/controller.ts | 7 + server/src/modules/apps/service.ts | 9 +- server/src/modules/apps/types/index.ts | 1 + .../src/modules/audit-logs/ability/index.ts | 1 + .../modules/audit-logs/constants/features.ts | 3 + .../src/modules/audit-logs/constants/index.ts | 1 + server/src/modules/audit-logs/types/index.ts | 1 + .../group-permissions/ability/index.ts | 9 +- .../group-permissions/constants/features.ts | 36 +- .../group-permissions/constants/index.ts | 9 +- .../modules/group-permissions/controller.ts | 13 +- .../granular-permissions.controller.ts | 62 +- .../interfaces/IController.ts | 17 +- .../group-permissions/interfaces/IService.ts | 26 +- .../src/modules/group-permissions/service.ts | 96 ++- .../services/granular-permissions.service.ts | 42 +- .../modules/group-permissions/types/index.ts | 9 +- .../constants/feature.ts | 12 +- .../import-export-resources/service.ts | 23 +- .../Interfaces/IController.ts | 3 +- .../instance-settings/Interfaces/IService.ts | 3 +- .../modules/instance-settings/controller.ts | 4 +- .../src/modules/instance-settings/module.ts | 3 +- .../src/modules/instance-settings/service.ts | 3 +- server/src/modules/licensing/controller.ts | 4 +- .../licensing/interfaces/IController.ts | 2 +- .../modules/licensing/interfaces/IService.ts | 3 +- server/src/modules/licensing/module.ts | 2 + server/src/modules/licensing/service.ts | 3 +- .../modules/login-configs/ability/index.ts | 1 + .../login-configs/constants/feature.ts | 11 +- .../modules/login-configs/constants/index.ts | 1 + .../src/modules/login-configs/controller.ts | 20 +- .../login-configs/interfaces/IController.ts | 6 +- .../login-configs/interfaces/IService.ts | 9 +- server/src/modules/login-configs/service.ts | 45 +- .../src/modules/login-configs/types/index.ts | 1 + .../modules/organizations/ability/index.ts | 3 +- .../organizations/constants/feature.ts | 15 +- .../modules/organizations/constants/index.ts | 3 +- .../src/modules/organizations/controller.ts | 29 +- .../organizations/interfaces/IController.ts | 11 +- .../organizations/interfaces/IService.ts | 9 +- server/src/modules/organizations/service.ts | 59 +- .../src/modules/organizations/types/index.ts | 3 +- .../src/modules/setup-organization/service.ts | 21 +- server/src/modules/smtp/controller.ts | 11 +- .../modules/smtp/interfaces/IController.ts | 7 +- .../src/modules/smtp/interfaces/IService.ts | 7 +- server/src/modules/smtp/module.ts | 3 +- server/src/modules/smtp/service.ts | 7 +- .../versions/ability/app-version.ability.ts | 9 + .../modules/versions/constants/features.ts | 4 + .../src/modules/versions/constants/index.ts | 3 + server/src/modules/versions/controller.ts | 4 +- server/src/modules/versions/controller.v2.ts | 2 +- server/src/modules/versions/service.ts | 2 + server/src/modules/versions/types/index.ts | 3 + .../white-labelling/Interfaces/IController.ts | 9 +- .../white-labelling/Interfaces/IService.ts | 3 +- .../src/modules/white-labelling/controller.ts | 7 +- server/src/modules/white-labelling/service.ts | 3 +- 94 files changed, 1103 insertions(+), 526 deletions(-) create mode 100644 resolve-conflicts-submodules.sh diff --git a/cypress-tests/cypress/commands/apiCommands.js b/cypress-tests/cypress/commands/apiCommands.js index 62d72f7cdc..c8ca26fd2f 100644 --- a/cypress-tests/cypress/commands/apiCommands.js +++ b/cypress-tests/cypress/commands/apiCommands.js @@ -221,21 +221,21 @@ Cypress.Commands.add( const requestBody = envVar === "Enterprise" ? { - email: userEmail, - firstName: userName, - groups: [], - lastName: "", - role: userRole, - userMetadata: metaData, - } + email: userEmail, + firstName: userName, + groups: [], + lastName: "", + role: userRole, + userMetadata: metaData, + } : { - email: userEmail, - firstName: userName, - groups: [], - lastName: "", - role: userRole, - userMetadata: metaData, - }; + email: userEmail, + firstName: userName, + groups: [], + lastName: "", + role: userRole, + userMetadata: metaData, + }; cy.getCookie("tj_auth_token").then((cookie) => { cy.request( @@ -509,7 +509,7 @@ Cypress.Commands.add("apiDeleteGranularPermission", (groupName) => { // Delete the granular permission cy.request({ method: "DELETE", - url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/${granularPermissionId}`, + url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/app/${granularPermissionId}`, headers, log: false, }).then((deleteResponse) => { diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js index d6483fa400..19b87c6efe 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js @@ -5,10 +5,7 @@ import { inviteUserToWorkspace } from "Support/utils/manageUsers"; import { setSignupStatus } from "Support/utils/manageSSO"; import { onboardingSelectors } from "Selectors/onboarding"; import { commonText } from "Texts/common"; -import { - userSignUp, - addNewUser, -} from "Support/utils/onboarding"; +import { userSignUp, addNewUser } from "Support/utils/onboarding"; import { setUpSlug, setupAppWithSlug, @@ -16,347 +13,371 @@ import { onboardUserFromAppLink, } from "Support/utils/apps"; +describe( + "Private and Public apps", + { + retries: { runMode: 2 }, + }, + () => { + let data; -describe("Private and Public apps", { - retries: { runMode: 2 }, -}, () => { - let data; + beforeEach(() => { + data = { + appName: `${fake.companyName} P P App`, + slug: `${fake.companyName} P P App`.toLowerCase().replace(/\s+/g, "-"), + firstName: fake.firstName, + email: fake.email.toLowerCase(), + workspaceName: fake.firstName, + workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"), + }; - beforeEach(() => { - data = { - appName: `${fake.companyName} P P App`, - slug: `${fake.companyName} P P App`.toLowerCase().replace(/\s+/g, "-"), - firstName: fake.firstName, - email: fake.email.toLowerCase(), - workspaceName: fake.firstName, - workspaceSlug: fake.firstName.toLowerCase().replace(/\s+/g, "-"), - } - - cy.defaultWorkspaceLogin(); - cy.skipWalkthrough(); - }); - - it("Verify private and public app share functionality", () => { - cy.apiCreateApp(data.appName); - cy.openApp(); - cy.apiAddComponentToApp(data.appName, "text1"); - - // Check unreleased version state - cy.get('[data-cy="share-button-link"]>span').should("be.visible").click(); - cy.contains("This version has not been released yet").should("be.visible"); - cy.get(commonWidgetSelector.modalCloseButton).click(); - - // Release and verify share modal - releaseApp(); - cy.get(commonWidgetSelector.shareAppButton).click(); - for (const elements in commonWidgetSelector.shareModalElements) { - cy.get(commonWidgetSelector.shareModalElements[elements]) - .verifyVisibleElement("have.text", commonText.shareModalElements[elements]); - } - - // Verify share modal elements - const shareModalSelectors = [ - 'copyAppLinkButton', - 'makePublicAppToggle', - 'appLink', - 'appNameSlugInput', - 'modalCloseButton' - ]; - shareModalSelectors.forEach(selector => { - cy.get(commonWidgetSelector[selector]).should("be.visible"); + cy.defaultWorkspaceLogin(); + cy.skipWalkthrough(); }); - // Configure and verify slug - cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug); - cy.get('[data-cy="app-slug-accepted-label"]') - .should("be.visible") - .and("have.text", "Slug accepted!"); + it("Verify private and public app share functionality", () => { + cy.apiCreateApp(data.appName); + cy.openApp(); + cy.apiAddComponentToApp(data.appName, "text1"); - cy.get(commonWidgetSelector.modalCloseButton).click(); - cy.forceClickOnCanvas(); - cy.backToApps(); + // Check unreleased version state + cy.get('[data-cy="share-button-link"]>span').should("be.visible").click(); + cy.contains("This version has not been released yet").should( + "be.visible" + ); + cy.get(commonWidgetSelector.modalCloseButton).click(); - // Test private access - logout(); + // Release and verify share modal + releaseApp(); + cy.get(commonWidgetSelector.shareAppButton).click(); + for (const elements in commonWidgetSelector.shareModalElements) { + cy.get( + commonWidgetSelector.shareModalElements[elements] + ).verifyVisibleElement( + "have.text", + commonText.shareModalElements[elements] + ); + } - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + // Verify share modal elements + const shareModalSelectors = [ + "copyAppLinkButton", + "makePublicAppToggle", + "appLink", + "appNameSlugInput", + "modalCloseButton", + ]; + shareModalSelectors.forEach((selector) => { + cy.get(commonWidgetSelector[selector]).should("be.visible"); + }); + + // Configure and verify slug + cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug); + cy.get('[data-cy="app-slug-accepted-label"]') + .should("be.visible") + .and("have.text", "Slug accepted!"); + + cy.get(commonWidgetSelector.modalCloseButton).click(); + cy.forceClickOnCanvas(); + cy.backToApps(); + + // Test private access + logout(); + + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); + + cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should( + "be.visible" + ); + cy.wait(2000); + cy.appUILogin(); + cy.get(commonWidgetSelector.draggableWidget("text1")).should( + "be.visible" + ); + + // Test public access + cy.get(commonSelectors.viewerPageLogo).click(); + cy.openApp( + "appSlug", + Cypress.env("workspaceId"), + Cypress.env("appId"), + commonWidgetSelector.draggableWidget("text1") + ); + cy.get(commonWidgetSelector.shareAppButton).click(); + cy.get(commonWidgetSelector.makePublicAppToggle).check(); + cy.get(commonWidgetSelector.modalCloseButton).click(); + cy.backToApps(); + + logout(); + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); + cy.get(commonWidgetSelector.draggableWidget("text1")).should( + "be.visible" + ); }); - cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible"); - cy.wait(2000); - cy.appUILogin(); - cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible"); + it("Verify app private and public app visibility for the same workspace user", () => { + setupAppWithSlug(data.appName, data.slug); + inviteUserToWorkspace(data.firstName, data.email); + logout(); + cy.visit("/"); + cy.wait(2000); + cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should( + "be.visible" + ); - // Test public access - cy.get(commonSelectors.viewerPageLogo).click(); - cy.openApp( - "appSlug", - Cypress.env("workspaceId"), - Cypress.env("appId"), - commonWidgetSelector.draggableWidget("text1") - ); - cy.get(commonWidgetSelector.shareAppButton).click(); - cy.get(commonWidgetSelector.makePublicAppToggle).check(); - cy.get(commonWidgetSelector.modalCloseButton).click(); - cy.backToApps(); + // Test private access + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); - logout(); - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, - }); - cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible"); + cy.wait(2000); + cy.appUILogin(data.email, "password"); + cy.get(commonWidgetSelector.draggableWidget("text1")).should( + "be.visible" + ); - }); + // Test with private app valid session + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); + cy.get(commonWidgetSelector.draggableWidget("text1")).should( + "be.visible" + ); - it("Verify app private and public app visibility for the same workspace user", () => { - setupAppWithSlug(data.appName, data.slug); + cy.get(commonSelectors.viewerPageLogo).click(); - inviteUserToWorkspace(data.firstName, data.email); - logout(); - cy.visit("/"); - cy.wait(2000); - cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible"); + // Test public access + cy.defaultWorkspaceLogin(); + cy.wait(1000); + cy.apiMakeAppPublic(); + logout(); + cy.wait(1000); + cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should( + "be.visible" + ); - // Test private access - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); + cy.get(commonWidgetSelector.draggableWidget("text1")).should( + "be.visible" + ); + + // Test with public app with valid session + cy.apiLogin(data.email, "password"); + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); + cy.get(commonWidgetSelector.draggableWidget("text1")).should( + "be.visible" + ); }); - cy.wait(2000); - cy.appUILogin(data.email, "password"); + it("Verify app private and public app visibility for the same instance user", () => { + setupAppWithSlug(data.appName, data.slug); - cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible"); + cy.apiLogout(); + userSignUp(data.firstName, data.email, data.workspaceName); + cy.wait(1000); + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); - // Test with private app valid session - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, - }); - cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible"); + cy.visit("/"); + logout(); + // Test public access + cy.defaultWorkspaceLogin(); + cy.apiMakeAppPublic(); + logout(); - cy.get(commonSelectors.viewerPageLogo).click(); + cy.wait(1000); + cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should( + "be.visible" + ); - // Test public access - cy.defaultWorkspaceLogin(); - cy.wait(1000); - cy.apiMakeAppPublic(); - logout(); - cy.wait(1000); - cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible"); + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); + cy.get(commonWidgetSelector.draggableWidget("text1")).should( + "be.visible" + ); - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, - }); - cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible"); - - - - // Test with public app with valid session - cy.apiLogin(data.email, "password"); - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, - }); - cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible"); - - - }); - - it("Verify app private and public app visibility for the same instance user", () => { - setupAppWithSlug(data.appName, data.slug); - - cy.apiLogout(); - userSignUp(data.firstName, data.email, data.workspaceName); - cy.wait(1000); - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + // Verify public app with valid session + cy.apiLogin(data.email, "password"); + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); + cy.get(commonWidgetSelector.draggableWidget("text1")).should( + "be.visible" + ); }); - cy.visit("/"); - logout(); + it("Should redirect to workspace login and handle signup flow of existing and non-existing user", () => { + setSignupStatus(true); + setupAppWithSlug(data.appName, data.slug); - // Test public access - cy.defaultWorkspaceLogin(); - cy.apiMakeAppPublic(); - logout(); + cy.apiLogout(); + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); - cy.wait(1000); - cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible"); + cy.get(commonSelectors.workspaceName).verifyVisibleElement( + "have.text", + "My workspace" + ); - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, - }); - cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible"); + // Test signup flow + cy.intercept("POST", "/api/onboarding/signup").as("signup"); + cy.get(commonSelectors.createAnAccountLink).click(); + cy.wait(3000); + cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName); + cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email); + cy.clearAndType(onboardingSelectors.loginPasswordInput, "password"); + cy.get(commonSelectors.signUpButton).click(); + cy.wait("@signup").then((interception) => { + expect(interception.response.statusCode).to.eq(201); + }); + // Process invitation + onboardUserFromAppLink(data.email, data.slug); - // Verify public app with valid session - cy.apiLogin(data.email, "password"); - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, - }); - cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible"); + cy.get(commonWidgetSelector.draggableWidget("text1")).should( + "be.visible" + ); + cy.get('[data-cy="viewer-page-logo"]').click(); + logout(); + cy.wait(1000); + cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should( + "be.visible" + ); - }); + // Setup new workspace and app + cy.defaultWorkspaceLogin(); + cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug); + cy.apiLogout(); + cy.apiLogin(); + cy.visit(`${data.workspaceSlug}`); + setSignupStatus(true, data.workspaceName); - it("Should redirect to workspace login and handle signup flow of existing and non-existing user", () => { - setSignupStatus(true); - setupAppWithSlug(data.appName, data.slug); + data.slug = fake.firstName.toLowerCase().replace(/\s+/g, "-"); - cy.apiLogout(); - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + cy.createApp(data.appName); + cy.dragAndDropWidget("Text", 500, 500); + releaseApp(); + setUpSlug(data.slug); + cy.forceClickOnCanvas(); + cy.backToApps(); + + // Test signup flow in new workspace + cy.apiLogout(); + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); + + cy.get(commonSelectors.workspaceName).verifyVisibleElement( + "have.text", + data.workspaceName + ); + + cy.get(commonSelectors.createAnAccountLink).click(); + cy.wait(3000); + cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName); + cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email); + cy.clearAndType(onboardingSelectors.loginPasswordInput, "password"); + cy.get(commonSelectors.signUpButton).click(); + cy.wait("@signup").then((interception) => { + expect(interception.response.statusCode).to.eq(201); + }); + + onboardUserFromAppLink(data.email, data.slug, data.workspaceName, false); + cy.get(commonWidgetSelector.draggableWidget("text1")).should( + "be.visible" + ); }); - cy.get(commonSelectors.workspaceName).verifyVisibleElement( - "have.text", - "My workspace" - ); + it("Should verify restricted app access", () => { + data.workspaceName = fake.firstName; + data.workspaceSlug = fake.firstName.toLowerCase().replace(/\s+/g, "-"); - // Test signup flow - cy.intercept("POST", "/api/onboarding/signup").as("signup"); - cy.get(commonSelectors.createAnAccountLink).click(); - cy.wait(3000); - cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName); - cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email); - cy.clearAndType(onboardingSelectors.loginPasswordInput, "password"); - cy.get(commonSelectors.signUpButton).click(); + cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug); + cy.apiLogout(); + cy.apiLogin(); + cy.visit(`${data.workspaceSlug}`); + cy.apiDeleteGranularPermission("end-user"); + setSignupStatus(true, data.workspaceName); - cy.wait('@signup').then((interception) => { - expect(interception.response.statusCode).to.eq(201); + setupAppWithSlug(data.appName, data.slug); + + inviteUserToWorkspace(data.firstName, data.email); + + // Verify restricted access + cy.visitSlug({ + actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }); + verifyRestrictedAccess(); + cy.get('[data-cy="back-to-home-button"]').click(); + cy.get(commonSelectors.homePageLogo).should("be.visible"); + + cy.apiLogout(); }); - // Process invitation - onboardUserFromAppLink(data.email, data.slug); + it.skip("Should verify private app access for different workspace users", () => { + const firstName1 = fake.firstName; + const email1 = fake.email.toLowerCase(); + const permissionName = fake.firstName.toLowerCase(); // Defined but not used in original + const urls = { + editor: `${Cypress.config("baseUrl")}/my-workspace/apps/${data.slug}/home`, + preview: `${Cypress.config("baseUrl")}/applications/${data.slug}/home?version=v1`, + released: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + }; - cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible"); + // Setup workspace and app + cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug); + cy.apiLogout(); + cy.apiLogin(); + cy.visit(`${data.workspaceSlug}`); + setupAppWithSlug(data.appName, data.slug); + // Invite workspace user + addNewUser(data.firstName, data.email); + cy.wait(500); - cy.get('[data-cy="viewer-page-logo"]').click(); - logout(); - cy.wait(1000); - cy.get(onboardingSelectors.signInButton, { timeout: 20000 }).should("be.visible"); + // Verify access restrictions + cy.visitSlug({ actualUrl: urls.editor }); + verifyRestrictedAccess(); + cy.get('[data-cy="back-to-home-button"]').click(); + cy.get(commonSelectors.homePageLogo).should("be.visible"); - // Setup new workspace and app - cy.defaultWorkspaceLogin(); - cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug); - cy.apiLogout(); - cy.apiLogin(); - cy.visit(`${data.workspaceSlug}`); - setSignupStatus(true, data.workspaceName); + cy.visitSlug({ actualUrl: urls.preview }); - data.slug = fake.firstName.toLowerCase().replace(/\s+/g, "-"); + // Switch users and verify access + cy.apiLogout(); + cy.apiLogin(); + cy.apiDeleteGranularPermission("end-user"); - cy.createApp(data.appName); - cy.dragAndDropWidget("Text", 500, 500); - releaseApp(); - setUpSlug(data.slug); - cy.forceClickOnCanvas(); - cy.backToApps(); + cy.apiLogin(data.email, "password"); + cy.visitSlug({ actualUrl: urls.editor }); + verifyRestrictedAccess(); + cy.get('[data-cy="back-to-home-button"]').click(); + cy.get(commonSelectors.homePageLogo).should("be.visible"); + cy.visitSlug({ actualUrl: urls.preview }); - // Test signup flow in new workspace - cy.apiLogout(); - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, + cy.apiLogout(); + + // Test with new user + userSignUp(firstName1, email1, data.workspaceName); + cy.visitSlug({ actualUrl: urls.editor }); + cy.visitSlug({ actualUrl: urls.preview }); + cy.visitSlug({ actualUrl: urls.released }); }); - - cy.get(commonSelectors.workspaceName).verifyVisibleElement( - "have.text", - data.workspaceName - ); - - cy.get(commonSelectors.createAnAccountLink).click(); - cy.wait(3000); - cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName); - cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email); - cy.clearAndType(onboardingSelectors.loginPasswordInput, "password"); - cy.get(commonSelectors.signUpButton).click(); - cy.wait('@signup').then((interception) => { - expect(interception.response.statusCode).to.eq(201); - }); - - onboardUserFromAppLink(data.email, data.slug, data.workspaceName, false); - cy.get(commonWidgetSelector.draggableWidget("text1")).should("be.visible"); - - - }); - - it("Should verify restricted app access", () => { - data.workspaceName = fake.firstName; - data.workspaceSlug = fake.firstName.toLowerCase().replace(/\s+/g, "-"); - - cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug); - cy.apiLogout(); - cy.apiLogin(); - cy.visit(`${data.workspaceSlug}`); - cy.apiDeleteGranularPermission("end-user"); - setSignupStatus(true, data.workspaceName); - - setupAppWithSlug(data.appName, data.slug); - - inviteUserToWorkspace(data.firstName, data.email); - - // Verify restricted access - cy.visitSlug({ - actualUrl: `${Cypress.config("baseUrl")}/applications/${data.slug}`, - }); - verifyRestrictedAccess(); - cy.get('[data-cy="back-to-home-button"]').click(); - cy.get(commonSelectors.homePageLogo).should("be.visible"); - - cy.apiLogout(); - }); - - it.skip("Should verify private app access for different workspace users", () => { - const firstName1 = fake.firstName; - const email1 = fake.email.toLowerCase(); - const permissionName = fake.firstName.toLowerCase(); // Defined but not used in original - const urls = { - editor: `${Cypress.config("baseUrl")}/my-workspace/apps/${data.slug}/home`, - preview: `${Cypress.config("baseUrl")}/applications/${data.slug}/home?version=v1`, - released: `${Cypress.config("baseUrl")}/applications/${data.slug}` - }; - - // Setup workspace and app - cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug); - cy.apiLogout(); - cy.apiLogin(); - cy.visit(`${data.workspaceSlug}`); - setupAppWithSlug(data.appName, data.slug); - - // Invite workspace user - addNewUser(data.firstName, data.email); - cy.wait(500); - - // Verify access restrictions - cy.visitSlug({ actualUrl: urls.editor }); - verifyRestrictedAccess(); - cy.get('[data-cy="back-to-home-button"]').click(); - cy.get(commonSelectors.homePageLogo).should("be.visible"); - - cy.visitSlug({ actualUrl: urls.preview }); - - // Switch users and verify access - cy.apiLogout(); - cy.apiLogin(); - cy.apiDeleteGranularPermission("end-user"); - - cy.apiLogin(data.email, "password"); - cy.visitSlug({ actualUrl: urls.editor }); - verifyRestrictedAccess(); - cy.get('[data-cy="back-to-home-button"]').click(); - cy.get(commonSelectors.homePageLogo).should("be.visible"); - cy.visitSlug({ actualUrl: urls.preview }); - - cy.apiLogout(); - - // Test with new user - userSignUp(firstName1, email1, data.workspaceName); - cy.visitSlug({ actualUrl: urls.editor }); - cy.visitSlug({ actualUrl: urls.preview }); - cy.visitSlug({ actualUrl: urls.released }); - }); -}); \ No newline at end of file + } +); diff --git a/cypress-tests/cypress/support/utils/manageGroups.js b/cypress-tests/cypress/support/utils/manageGroups.js index 3f0c85d1cf..2ddd80379d 100644 --- a/cypress-tests/cypress/support/utils/manageGroups.js +++ b/cypress-tests/cypress/support/utils/manageGroups.js @@ -658,7 +658,7 @@ export const createGroupAddAppAndUserToGroup = (groupName, email) => { cy.request({ method: "POST", - url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions`, + url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions/app`, headers: headers, body: { name: "Apps", @@ -676,7 +676,6 @@ export const createGroupAddAppAndUserToGroup = (groupName, email) => { ], }, }, - }).then((response) => { expect(response.status).to.equal(201); }); @@ -727,7 +726,7 @@ export const duplicateMultipleGroups = (groupNames) => { cy.get(commonSelectors.duplicateOption).click(); // Click on the duplicate option cy.get(commonSelectors.confirmDuplicateButton).click(); // Confirm duplication if needed }); -} +}; export const verifyGroupCardOptions = (groupName) => { cy.get(groupsSelector.groupLink(groupName)).click(); @@ -865,7 +864,7 @@ export const addUserInGroup = (groupName, email) => { commonSelectors.toastMessage, groupsText.userAddedToast ); -} +}; export const inviteUserBasedOnRole = (firstName, email, role = "end-user") => { fillUserInviteForm(firstName, email); @@ -891,7 +890,13 @@ export const verifyBasicPermissions = (canCreate = true) => { ); }; -export const setupWorkspaceAndInviteUser = (workspaceName, workspaceSlug, firstName, email, role = "end-user") => { +export const setupWorkspaceAndInviteUser = ( + workspaceName, + workspaceSlug, + firstName, + email, + role = "end-user" +) => { cy.apiCreateWorkspace(workspaceName, workspaceSlug); cy.visit(workspaceSlug); cy.wait(1000); @@ -907,7 +912,10 @@ export const verifySettingsAccess = (shouldExist = true) => { ); }; -export const verifyUserPrivileges = (expectedButtonState, shouldHaveWorkspaceSettings) => { +export const verifyUserPrivileges = ( + expectedButtonState, + shouldHaveWorkspaceSettings +) => { cy.get(commonSelectors.dashboardAppCreateButton).should(expectedButtonState); cy.get(commonSelectors.settingsIcon).click(); @@ -923,4 +931,4 @@ export const setupAndUpdateRole = (currentRole, endRole, email) => { updateRole(currentRole, endRole, email); cy.wait(1000); cy.apiLogout(); -}; \ No newline at end of file +}; diff --git a/frontend/ee b/frontend/ee index 8aaec8a424..961eee8320 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 8aaec8a4242f4a24fe7d28768e0c4341f50def82 +Subproject commit 961eee83206a2ad6492aba5782b381aeb7c238f7 diff --git a/frontend/src/AppBuilder/QueryManager/Components/DataSourcePicker.jsx b/frontend/src/AppBuilder/QueryManager/Components/DataSourcePicker.jsx index 22d6fcca28..1c6ebe9ebd 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/DataSourcePicker.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/DataSourcePicker.jsx @@ -93,7 +93,7 @@ function DataSourcePicker({ darkMode }) { documentation diff --git a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx index af76275575..f14f1cdefb 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/QueryManagerBody.jsx @@ -280,10 +280,10 @@ export const BaseQueryManagerBody = ({ darkMode, activeTab, renderCopilot = () = } const isSampleDb = selectedDataSource?.type === DATA_SOURCE_TYPE.SAMPLE; const docLink = isSampleDb - ? 'https://docs.tooljet.com/docs/data-sources/sample-data-sources' + ? 'https://docs.tooljet.ai/docs/data-sources/sample-data-sources' : selectedDataSource?.plugin_id && selectedDataSource.plugin_id.trim() !== '' - ? `https://docs.tooljet.com/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/` - : `https://docs.tooljet.com/docs/data-sources/${selectedDataSource?.kind}`; + ? `https://docs.tooljet.ai/docs/marketplace/plugins/marketplace-plugin-${selectedDataSource?.kind}/` + : `https://docs.tooljet.ai/docs/data-sources/${selectedDataSource?.kind}`; return ( <>
diff --git a/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx b/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx index e2c2ab56c8..0c39e917c3 100644 --- a/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx +++ b/frontend/src/AppBuilder/QueryManager/Components/Transformation.jsx @@ -70,7 +70,7 @@ const EducativeLabel = ({ darkMode }) => { faster. It uses OpenAI's GPT-3.5 to suggest queries based on your data.