diff --git a/.vscode/settings.json b/.vscode/settings.json index cfe0f9a..6caeac7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,28 +5,15 @@ "**/.hg": false, "**/CVS": false, "**/__pycache__": false, - "ui/*README": false, - "ui/*README.*": false, - "ui/*config": false, - "ui/*config.*": false, - "ui/*node_modules": false, - "ui/*node_modules.*": false, - "ui/*package-lock": false, - "ui/*package-lock.*": false, - "ui/*package": false, - "ui/*package.*": false, - "ui/*public": false, - "ui/*public.*": false, - "ui/*scripts.*": false, - "ui/*src": false, - "ui/*src.*": false, - "ui/*tsconfig": false, - "ui/*tsconfig.*": false, - "ui/*yarn": false, - "ui/*yarn.*": false, - "**/*.map": true, - "**/*.js": true, - ".git": true + "*.DS_Store": false, + "*.DS_Store.*": false, + "*.git": false, + "*.git.*": false, + "*.vscode": false, + "*.vscode.*": false, + "*server.*": false, + "*ui": false, + "*ui.*": false }, "cSpell.words": [ "uuidv" diff --git a/server/src/core/controllers/app.ts b/server/src/core/controllers/app.ts index 516df5b..23edea4 100644 --- a/server/src/core/controllers/app.ts +++ b/server/src/core/controllers/app.ts @@ -4,20 +4,23 @@ import cors from "cors"; import locator from "../di/register_di"; import { DevEnv, UnitTestEnv } from "../di/env"; import mongoose from "mongoose"; -// import http from "http"; -// import { Server } from "socket.io"; +import { Server } from "socket.io"; +import { createServer } from "http"; +import { TypedEvent } from "../helper/typed_event"; +import { SocketSubscriber } from "./socket_controller"; export class App { public app: express.Application; public port: number; public env: string; - public computedFolder: string; - // public io: - constructor(routes: Routes[], computedFolder: string) { - this.port = 3000; + public socketSubscribers: SocketSubscriber[]; + public io: Server; + + constructor(routes: Routes[], socketSubscribers: SocketSubscriber[]) { + this.port = 4001; + this.socketSubscribers = socketSubscribers; this.env = "dev"; this.app = express(); - this.computedFolder = computedFolder; this.loadAppDependencies().then(() => { this.initializeMiddlewares(); this.initializeRoutes(routes); @@ -25,26 +28,27 @@ export class App { } public listen() { - // const httpServer = new http.Server(); - // const io = new Server(httpServer); + const httpServer = createServer(this.app); + const io = new Server(httpServer, { + cors: { origin: "*" }, + }); - this.app.listen(this.port, () => { + io.on("connection", (socket) => { + this.socketSubscribers.map((el) => { + el.emitter.on((e) => { + socket.emit(el.event, e); + }); + }); + }); + + httpServer.listen(this.port, () => { console.info(`=================================`); console.info(`======= ENV: ${this.env} =======`); console.info(`🚀 HTTP http://localhost:${this.port}`); console.info(`🚀 WS ws://localhost:${this.port}`); console.info(`=================================`); }); - // io.on("connection", (socket) => { - // socket.on("disconnect", function (msg) { - // console.log("Disconnected"); - // }); - // }); - - // setInterval(function () { - // io.emit("goodbye"); - // console.log(200); - // }, 1000); + this.io = io; } public getServer() { @@ -63,20 +67,17 @@ export class App { }); } async loadAppDependencies() { - await locator( - this.env == "development" - ? new DevEnv(this.computedFolder) - : new UnitTestEnv(this.computedFolder) - ); + // await locator( + // this.env == "development" + // ? new DevEnv(this.computedFolder) + // : new UnitTestEnv(this.computedFolder) + // ); mongoose .connect("mongodb://127.0.0.1:27017/test") - .then(() => console.log("Connected!")) + .then(() => {}) .catch((e) => { console.log("ERROR:", e); }); } } - - - \ No newline at end of file diff --git a/server/src/core/controllers/socket_controller.ts b/server/src/core/controllers/socket_controller.ts index 9e20248..e78a457 100644 --- a/server/src/core/controllers/socket_controller.ts +++ b/server/src/core/controllers/socket_controller.ts @@ -1,15 +1,11 @@ -// import path from "path"; -// import { TypedEvent } from "../helper/typed_event"; -// import { StackService } from "../services/stack_service"; -// // TODO(IDONTSUDO): up to do +import { TypedEvent } from "../helper/typed_event"; -// class SocketController{ -// emitter:TypedEvent; -// constructor(emitter:TypedEvent, ){ -// this.emitter = emitter -// } -// call = () =>{ - -// } -// } - \ No newline at end of file +export class SocketSubscriber { + emitter: TypedEvent; + event: string; + constructor(emitter: TypedEvent, event: string) { + this.emitter = emitter; + this.event = event; + } +} + \ No newline at end of file diff --git a/server/src/core/middlewares/validation_model.ts b/server/src/core/middlewares/validation_model.ts index fbcfaac..9699182 100644 --- a/server/src/core/middlewares/validation_model.ts +++ b/server/src/core/middlewares/validation_model.ts @@ -17,7 +17,6 @@ export const validationModelMiddleware = ( } const model = plainToInstance(type, req[value]); validate(model, { skipMissingProperties, whitelist, forbidNonWhitelisted }).then((errors: ValidationError[]) => { - console.log(errors) if (errors.length > 0) { const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(', '); return res.status(400).json(message) diff --git a/server/src/core/model/exec_error_model.ts b/server/src/core/model/exec_error_model.ts index d9688cc..ab5de38 100644 --- a/server/src/core/model/exec_error_model.ts +++ b/server/src/core/model/exec_error_model.ts @@ -1,22 +1,19 @@ export class ExecError extends Error { - static isExecError(e: any) { - if ("type" in e && "script" in e && "unixTime" in e && 'error' in e) { + static isExecError(e: any): ExecError | void { + if ("type" in e && "script" in e && "unixTime" in e && "error" in e) { return new ExecError(e.type, e.event, e.data); } - return; + return; } script: string; unixTime: number; type = EXEC_TYPE.EXEC; - error:any; - constructor( - script: string, - ...args: any - ) { + error: any; + constructor(script: string, ...args: any) { super(...args); this.script = script; this.unixTime = Date.now(); - this.error = args[0] + this.error = args[0]; } } @@ -26,17 +23,13 @@ export class SpawnError extends Error { unixTime: number; type = EXEC_TYPE.SPAWN; - constructor( - environment: string, - script: string, - ...args: any - ) { + constructor(environment: string, script: string, ...args: any) { super(...args); this.environment = environment; this.script = script; this.unixTime = Date.now(); } - static isError(errorType: any): void | SpawnError { + static isError(errorType: any): SpawnError | void { if ( "command" in errorType && "error" in errorType && diff --git a/server/src/core/model/pipiline_meta.ts b/server/src/core/model/pipiline_meta.ts new file mode 100644 index 0000000..cc227ee --- /dev/null +++ b/server/src/core/model/pipiline_meta.ts @@ -0,0 +1,7 @@ +export interface IPipelineMeta { + pipelineIsRunning: boolean; + projectUUID?: string | null; + lastProcessCompleteCount: number | null; + error: any; + } + \ No newline at end of file diff --git a/server/src/core/model/process_model.ts b/server/src/core/model/process_model.ts index e5fa05f..c94b661 100644 --- a/server/src/core/model/process_model.ts +++ b/server/src/core/model/process_model.ts @@ -3,7 +3,7 @@ import { EXEC_TYPE } from "./exec_error_model"; export interface IPipeline { process: IProcess; - trigger: Trigger; + trigger?: Trigger; env: Env | null; stackGenerateType: StackGenerateType; } diff --git a/server/src/core/services/executor_program_service.ts b/server/src/core/services/executor_program_service.ts index 6107d7d..67b4666 100644 --- a/server/src/core/services/executor_program_service.ts +++ b/server/src/core/services/executor_program_service.ts @@ -9,9 +9,11 @@ import { EXEC_TYPE, ExecError, SpawnError } from "../model/exec_error_model"; abstract class IExecutorProgramService { abstract execPath: string; } +class P{ +} export class ExecutorProgramService - extends TypedEvent> + extends TypedEvent> implements IExecutorProgramService { static event = "ExecutorProgramService"; diff --git a/server/src/core/services/pipeline_real_time_service.ts b/server/src/core/services/pipeline_real_time_service.ts new file mode 100644 index 0000000..640a705 --- /dev/null +++ b/server/src/core/services/pipeline_real_time_service.ts @@ -0,0 +1,81 @@ +import { TypedEvent } from "../helper/typed_event"; +import { ExecError } from "../model/exec_error_model"; +import { ExecutorResult } from "../model/executor_result"; +import { IPipelineMeta } from "../model/pipiline_meta"; +import { IPipeline } from "../model/process_model"; +import { Iteration, StackService } from "./stack_service"; + + +export class PipelineRealTimeService extends TypedEvent { + status: IPipelineMeta; + pipelineModels?: IPipeline[]; + constructor() { + super(); + this.init(); + } + init() { + this.status = { + pipelineIsRunning: false, + projectUUID: null, + lastProcessCompleteCount: null, + error: null, + }; + } + pipelineSubscriber = (iterations: Iteration[]): void => { + if (this.status["lastProcessCompleteCount"] === 0) { + this.status["lastProcessCompleteCount"] = 0; + } + this.status.lastProcessCompleteCount += 1; + this.pipelineCompleted(); + this.iterationHelper(iterations[iterations.length - 1]); + this.emit(this.status); + }; + + iterationHelper(iteration: Iteration): void { + this.iterationErrorObserver(iteration); + + // TODO(IDONTSUDO): implements + // this.iterationLogSaver() + } + + iterationLogSaver() { + throw new Error("Method not implemented."); + } + + iterationErrorObserver(iteration: Iteration): void { + if (this.status.lastProcessCompleteCount === 1) { + return; + } + const result = iteration?.result; + const executorResult = ExecutorResult.isExecutorResult(result); + const execError = ExecError.isExecError(result); + + if (executorResult instanceof ExecutorResult) { + this.status.error = executorResult; + } + + if (execError instanceof ExecError) { + this.status.error = execError; + } + } + pipelineCompleted() { + const pipelineIsCompleted = + this.status.lastProcessCompleteCount === this.pipelineModels?.length; + if (pipelineIsCompleted) { + this.status.pipelineIsRunning = false; + } + } + + runPipeline( + pipelineModels: IPipeline[], + path: string, + projectUUID: string + ): void { + const testPath = path + "/context"; + const stack = new StackService(pipelineModels, testPath); + this.status["projectUUID"] = projectUUID; + this.status["pipelineIsRunning"] = true; + stack.on(this.pipelineSubscriber); + stack.call(); + } +} diff --git a/server/src/core/services/stack_service.ts b/server/src/core/services/stack_service.ts index ceb6fbf..d6f5e33 100644 --- a/server/src/core/services/stack_service.ts +++ b/server/src/core/services/stack_service.ts @@ -24,7 +24,10 @@ export abstract class IStackService { abstract init(processed: IPipeline[], path: string): void; } -export class StackService extends TypedEvent implements IStackService { +export class StackService + extends TypedEvent + implements IStackService +{ callStack: Iteration[]; path: string; constructor(processed: IPipeline[], path: string) { @@ -56,6 +59,7 @@ export class StackService extends TypedEvent implements IStackService { for await (const el of this.callStack!) { await this.execStack(inc, el); inc += 1; + this.emit(this.callStack); } } async execStack( @@ -121,14 +125,17 @@ export class StackService extends TypedEvent implements IStackService { return promise; } private async triggerExec( - trigger: Trigger, + trigger: Trigger | null, stackNumber: number ): Promise> { - const hashes = this.callStack[stackNumber].hashes; + if (trigger !== null) { + const hashes = this.callStack[stackNumber].hashes; - if (hashes != null) { - return await new TriggerService(trigger, hashes, this.path).call(); + if (hashes != null) { + return await new TriggerService(trigger, hashes, this.path).call(); + } + throw new Error("Hashes is null"); } - throw new Error("Hashes is null"); + return Result.ok(); } } diff --git a/server/src/core/usecases/search_database_model_usecase.ts b/server/src/core/usecases/search_database_model_usecase.ts new file mode 100644 index 0000000..860c9f0 --- /dev/null +++ b/server/src/core/usecases/search_database_model_usecase.ts @@ -0,0 +1,3 @@ +export class RegExpSearchDataBaseModelUseCase { + call = () => {}; +} diff --git a/server/src/features/pipelines/pipeline_model.ts b/server/src/features/pipelines/pipeline_model.ts index 3409dd0..4aeaec9 100644 --- a/server/src/features/pipelines/pipeline_model.ts +++ b/server/src/features/pipelines/pipeline_model.ts @@ -1,6 +1,6 @@ import { IsMongoId, IsEnum } from "class-validator"; import { Schema, model } from "mongoose"; -import { StackGenerateType } from "../../core/model/process_model"; +import { IProcess, StackGenerateType } from "../../core/model/process_model"; import { TriggerModel, triggerSchema, @@ -19,10 +19,7 @@ export const PipelineSchema = new Schema({ ref: triggerSchema, autopopulate: true, default: null, - }, - command: { - type: String, - }, + } }).plugin(require("mongoose-autopopulate")); export const schemaPipeline = "Pipeline"; @@ -34,7 +31,7 @@ export const PipelineDBModel = model( export class PipelineModel { @IsMongoId() - public process: PipelineModel; + public process: IProcess; @IsMongoId() //TODO(IDONTSUDO):NEED OPTION DECORATOR?? diff --git a/server/src/features/process/process_model.ts b/server/src/features/process/process_model.ts index df48590..92623dc 100644 --- a/server/src/features/process/process_model.ts +++ b/server/src/features/process/process_model.ts @@ -6,16 +6,16 @@ import { IsBoolean, } from "class-validator"; import { Schema, model } from "mongoose"; -import { - IProcess, - IssueType, -} from "../../core/model/process_model"; +import { IProcess, IssueType } from "../../core/model/process_model"; import { EXEC_TYPE } from "../../core/model/exec_error_model"; export const ProcessSchema = new Schema({ type: { type: String, }, + description: { + type: String, + }, command: { type: String, }, @@ -46,6 +46,9 @@ export class ProcessModel implements IProcess { @IsEnum(EXEC_TYPE) public type: EXEC_TYPE; + @IsString() + public description: string; + @IsString() public command: string; @@ -61,9 +64,8 @@ export class ProcessModel implements IProcess { @IsOptional() @IsNumber() public timeout?: number; - + @IsOptional() @IsString() public commit?: string; } - \ No newline at end of file diff --git a/server/src/features/projects/projects_model.ts b/server/src/features/projects/projects_model.ts index dfbf035..b359f59 100644 --- a/server/src/features/projects/projects_model.ts +++ b/server/src/features/projects/projects_model.ts @@ -5,6 +5,7 @@ import { IsMongoId, IsString } from "class-validator"; export interface IProjectModel { pipelines: [PipelineModel]; rootDir: string; + description: string; } export const ProjectSchema = new Schema({ @@ -17,6 +18,9 @@ export const ProjectSchema = new Schema({ rootDir: { type: String, }, + description: { + type: String, + }, }).plugin(require("mongoose-autopopulate")); const schema = "Projects"; @@ -25,7 +29,11 @@ export const ProjectDBModel = model(schema, ProjectSchema); export class ProjectModel implements IProjectModel { @IsMongoId() - pipelines: [PipelineModel]; + public pipelines: [PipelineModel]; + @IsString() - rootDir: string; + public rootDir: string; + + @IsString() + public description: string; } diff --git a/server/src/features/realtime/realtime_presentation.ts b/server/src/features/realtime/realtime_presentation.ts new file mode 100644 index 0000000..a1bbfbd --- /dev/null +++ b/server/src/features/realtime/realtime_presentation.ts @@ -0,0 +1,29 @@ +import { CoreHttpController } from "../../core/controllers/http_controller"; +import { Result } from "../../core/helper/result"; +import { IPipelineMeta } from "../../core/model/pipiline_meta"; +import { + PipelineRealTimeService, +} from "../../core/services/pipeline_real_time_service"; + +export const pipelineRealTimeService = new PipelineRealTimeService(); + +class PipelineStatusUseCase { + async call(): Promise> { + try { + return Result.ok(pipelineRealTimeService.status); + } catch (error) { + return Result.error(error as Error); + } + } +} + +export class RealTimePresentation extends CoreHttpController { + constructor() { + super({ + validationModel: null, + url: "realtime", + databaseModel: null, + }); + super.get(new PipelineStatusUseCase().call); + } +} diff --git a/server/src/features/triggers/trigger_model.ts b/server/src/features/triggers/trigger_model.ts index 5e25257..c1658e5 100644 --- a/server/src/features/triggers/trigger_model.ts +++ b/server/src/features/triggers/trigger_model.ts @@ -1,9 +1,10 @@ -import { IsArray, IsOptional, IsEnum} from "class-validator"; +import { IsArray, IsOptional, IsEnum, IsString } from "class-validator"; import { Schema, model } from "mongoose"; export interface ITriggerModel { _id?: string; type: string; + description: string; value: string[]; } @@ -12,6 +13,9 @@ export const TriggerSchema = new Schema({ type: String, require: true, }, + description: { + type: String, + }, value: { type: Array, require: true, @@ -20,7 +24,10 @@ export const TriggerSchema = new Schema({ export const triggerSchema = "Trigger"; -export const TriggerDBModel = model(triggerSchema, TriggerSchema); +export const TriggerDBModel = model( + triggerSchema, + TriggerSchema +); export enum TriggerType { PROCESS = "PROCESS", @@ -30,6 +37,8 @@ export enum TriggerType { export class TriggerModel implements ITriggerModel { @IsOptional() public _id: string; + @IsString() + public description; @IsEnum(TriggerType) public type: TriggerType; @IsArray() @@ -40,4 +49,3 @@ export interface Trigger { type: TriggerType; value: string[]; } - \ No newline at end of file diff --git a/server/src/main.ts b/server/src/main.ts index 3d844be..9b0a44f 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -5,17 +5,22 @@ import { TriggerPresentation } from "./features/triggers/triggers_presentation"; import { ProjectsPresentation } from "./features/projects/projects_presentation"; import { PipelinePresentation } from "./features/pipelines/pipeline_presentation"; import { ProcessPresentation } from "./features/process/process_presentation"; - +import { SocketSubscriber } from "./core/controllers/socket_controller"; +import { + RealTimePresentation, + pipelineRealTimeService, +} from "./features/realtime/realtime_presentation"; const httpRoutes: Routes[] = [ new TriggerPresentation(), new ProjectsPresentation(), new ProcessPresentation(), new PipelinePresentation(), + new RealTimePresentation(), ].map((el) => el.call()); -const computedFolder = ""; +const socketSubscribers = [ + new SocketSubscriber(pipelineRealTimeService, "realtime"), +]; -new App(httpRoutes, computedFolder).listen(); - - \ No newline at end of file +new App(httpRoutes, socketSubscribers).listen(); diff --git a/server/test/model/mock_pipelines.ts b/server/test/model/mock_pipelines.ts new file mode 100644 index 0000000..c166955 --- /dev/null +++ b/server/test/model/mock_pipelines.ts @@ -0,0 +1,46 @@ +import { EXEC_TYPE } from "../../src/core/model/exec_error_model"; +import { + IssueType, + StackGenerateType, +} from "../../src/core/model/process_model"; +import { TriggerType } from "../../src/features/triggers/trigger_model"; + +export const mockSimplePipeline = [ + { + process: { + type: EXEC_TYPE.EXEC, + command: `nix run gitlab:robossembler/nix-robossembler-overlay#test-script '{ + "filesMeta":[ + {"type":"folder","name":"example", "path": null,"rewrite":true} + ], + "path":"$PATH" + }'`, + isGenerating: true, + isLocaleCode: false, + issueType: IssueType.WARNING, + }, + trigger: { + type: TriggerType.FILE, + value: ["context"], + }, + env: null, + stackGenerateType: StackGenerateType.SINGLETON, + }, + { + process: { + type: EXEC_TYPE.EXEC, + command: `nix run gitlab:robossembler/nix-robossembler-overlay#test-script '{ + "filesMeta":[ + {"type":"file","name":"1.txt", "path":"example","rewrite":true} + ], + "path":"$PATH" + }'`, + isGenerating: true, + isLocaleCode: false, + issueType: IssueType.WARNING, + }, + trigger: null, + env: null, + stackGenerateType: StackGenerateType.SINGLETON, + }, +]; diff --git a/server/test/services/pipeline_real_time_service_test.ts b/server/test/services/pipeline_real_time_service_test.ts new file mode 100644 index 0000000..aa8a6ef --- /dev/null +++ b/server/test/services/pipeline_real_time_service_test.ts @@ -0,0 +1,13 @@ +import { PipelineRealTimeService } from "../../src/core/services/pipeline_real_time_service"; +import { mockSimplePipeline } from "../model/mock_pipelines"; +import { dirname__ } from "../test"; + +export class PipelineRealTimeServiceTest extends PipelineRealTimeService { + constructor() { + super(); + this.init(); + } + async test() { + this.runPipeline(mockSimplePipeline, dirname__, ""); + } +} diff --git a/server/test/services/stack_service_test.ts b/server/test/services/stack_service_test.ts index 12a25f8..8c3cce5 100644 --- a/server/test/services/stack_service_test.ts +++ b/server/test/services/stack_service_test.ts @@ -1,15 +1,9 @@ import { rmSync } from "fs"; import * as fs from "fs"; - -import { - IssueType, - StackGenerateType, -} from "../../src/core/model/process_model"; -import { EXEC_TYPE } from "../../src/core/model/exec_error_model"; import { StackService } from "../../src/core/services/stack_service"; import { delay } from "../../src/core/helper/delay"; import { assert, dirname__ } from "../test"; -import { TriggerType } from "../../src/features/triggers/trigger_model"; +import { mockSimplePipeline } from "../model/mock_pipelines"; abstract class IStackServiceTest { abstract test(): Promise; @@ -40,51 +34,7 @@ class SimpleTestStackServiceTest implements IStackServiceTest { constructor() { - super( - [ - { - process: { - type: EXEC_TYPE.EXEC, - command: `nix run gitlab:robossembler/nix-robossembler-overlay#test-script '{ - "filesMeta":[ - {"type":"folder","name":"example", "path": null,"rewrite":true} - ], - "path":"$PATH" - }'`, - isGenerating: true, - isLocaleCode: false, - issueType: IssueType.WARNING, - }, - trigger: { - type: TriggerType.FILE, - value: ["context"], - }, - env: null, - stackGenerateType: StackGenerateType.SINGLETON, - }, - { - process: { - type: EXEC_TYPE.EXEC, - command: `nix run gitlab:robossembler/nix-robossembler-overlay#test-script '{ - "filesMeta":[ - {"type":"file","name":"1.txt", "path":"example","rewrite":true} - ], - "path":"$PATH" - }'`, - isGenerating: true, - isLocaleCode: false, - issueType: IssueType.WARNING, - }, - trigger: { - type: TriggerType.FILE, - value: ["1.txt"], - }, - env: null, - stackGenerateType: StackGenerateType.SINGLETON, - }, - ], - dirname__ + "/context/" - ); + super(mockSimplePipeline, dirname__ + "/context/"); } async test(): Promise { await this.call(); diff --git a/server/test/test.ts b/server/test/test.ts index f29630a..a1056f0 100644 --- a/server/test/test.ts +++ b/server/test/test.ts @@ -12,6 +12,7 @@ import { DeleteDataBaseModelUseCaseTest } from "./usecases/delete_database_model import { ReadDataBaseModelUseCaseTest } from "./usecases/read_database_model_usecase_test"; import { UpdateDataBaseModelUseCaseTest } from "./usecases/update_database_model_usecase"; import { PaginationDataBaseModelUseCaseTest } from "./usecases/pagination_database_model_usecase_test"; +// import { PipelineRealTimeServiceTest } from "./services/pipeline_real_time_service_test"; const testCore = TestCore.instance; @@ -29,19 +30,20 @@ const init = async () =>{ } const test = async () =>{ - await new ExecutorProgramServiceTest(dirname__).test(); - await new FilesChangerTest(dirname__).test(); + // await new ExecutorProgramServiceTest(dirname__).test(); + // await new FilesChangerTest(dirname__).test(); await new StackServiceTest(dirname__ + "/context/").test(); - await new TriggerServiceTest().test(); - await new CreateDataBaseModelUseCaseTest().test() + // await new TriggerServiceTest().test(); + // await new CreateDataBaseModelUseCaseTest().test() - await new CreateDataBaseModelUseCaseTest().test() - await new DeleteDataBaseModelUseCaseTest().test() - await new ReadDataBaseModelUseCaseTest().test() - await new UpdateDataBaseModelUseCaseTest().test() - for await (const usecase of tests) { - testCore.assert(await new usecase().test(), usecase.name) - } + // await new CreateDataBaseModelUseCaseTest().test() + // await new DeleteDataBaseModelUseCaseTest().test() + // await new ReadDataBaseModelUseCaseTest().test() + // await new UpdateDataBaseModelUseCaseTest().test() + // await new PipelineRealTimeServiceTest().test() + // for await (const usecase of tests) { + // testCore.assert(await new usecase().test(), usecase.name) + // } } const main = async () => { await init() diff --git a/server/tsconfig.json b/server/tsconfig.json index f5e853d..df616d5 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,19 +1,19 @@ { - "compileOnSave": false, - "compilerOptions": { - "target": "es2017", - "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "pretty": true, - "declaration": true, - "outDir": "./build", - "allowJs": true, - "noEmit": false, - "esModuleInterop": true, - "resolveJsonModule": true, - "importHelpers": true, - } - } \ No newline at end of file + "compileOnSave": false, + "compilerOptions": { + "target": "es2017", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "pretty": true, + "declaration": true, + "outDir": "./build", + "allowJs": true, + "noEmit": false, + "esModuleInterop": true, + "resolveJsonModule": true, + "importHelpers": true + } +} diff --git a/ui/.gitignore b/ui/.gitignore index f40a7a5..309bb04 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -21,4 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -package-lock.json \ No newline at end of file +package-lock.json +todo.md \ No newline at end of file diff --git a/ui/package.json b/ui/package.json index 8841f5f..4c422a8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,5 +1,5 @@ { - "name": "infinite-todo-list", + "name": "ui-robossembler", "version": "0.1.0", "private": true, "dependencies": { @@ -11,14 +11,20 @@ "@types/node": "^16.18.46", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", + "@types/socket.io-client": "^3.0.0", "@types/uuid": "^9.0.2", "formik-antd": "^2.0.4", + "i18next": "^23.6.0", "mobx": "^6.10.0", "mobx-react-lite": "^4.0.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^13.3.1", + "react-infinite-scroll-component": "^6.1.0", + "react-router-dom": "^6.18.0", "react-scripts": "5.0.1", "sass": "^1.66.1", + "socket.io-client": "^4.7.2", "typescript": "^4.9.5", "uuid": "^9.0.0", "web-vitals": "^2.1.4" diff --git a/ui/public/index.html b/ui/public/index.html index 29d71f6..3dac7ba 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -11,7 +11,7 @@ /> - Infinite Todo List + robossembler: pipeline diff --git a/ui/src/assets/icons/add.svg b/ui/src/core/assets/icons/add.svg similarity index 100% rename from ui/src/assets/icons/add.svg rename to ui/src/core/assets/icons/add.svg diff --git a/ui/src/assets/icons/check.svg b/ui/src/core/assets/icons/check.svg similarity index 100% rename from ui/src/assets/icons/check.svg rename to ui/src/core/assets/icons/check.svg diff --git a/ui/src/assets/icons/chevron.svg b/ui/src/core/assets/icons/chevron.svg similarity index 100% rename from ui/src/assets/icons/chevron.svg rename to ui/src/core/assets/icons/chevron.svg diff --git a/ui/src/assets/icons/delete.svg b/ui/src/core/assets/icons/delete.svg similarity index 100% rename from ui/src/assets/icons/delete.svg rename to ui/src/core/assets/icons/delete.svg diff --git a/ui/src/core/assets/icons/error.svg b/ui/src/core/assets/icons/error.svg new file mode 100644 index 0000000..3f02c89 --- /dev/null +++ b/ui/src/core/assets/icons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/core/assets/icons/left_icon.svg b/ui/src/core/assets/icons/left_icon.svg new file mode 100644 index 0000000..cf4c2af --- /dev/null +++ b/ui/src/core/assets/icons/left_icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/core/assets/icons/reload.svg b/ui/src/core/assets/icons/reload.svg new file mode 100644 index 0000000..2bdbecc --- /dev/null +++ b/ui/src/core/assets/icons/reload.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/assets/images/logo.svg b/ui/src/core/assets/images/logo.svg similarity index 100% rename from ui/src/assets/images/logo.svg rename to ui/src/core/assets/images/logo.svg diff --git a/ui/src/core/extensions/array.ts b/ui/src/core/extensions/array.ts new file mode 100644 index 0000000..cfbac22 --- /dev/null +++ b/ui/src/core/extensions/array.ts @@ -0,0 +1,45 @@ +export {}; + +declare global { + interface Array { + // @strict: The parameter is determined whether the arrays must be exactly the same in content and order of this relationship or simply follow the same requirements. + equals(array: Array, strict: boolean): boolean; + lastElement(): T | undefined; + } +} + +export const ArrayExtensions = () => { + if ([].equals === undefined) { + // eslint-disable-next-line no-extend-native + Array.prototype.equals = function (array, strict = true) { + if (!array) return false; + + if (arguments.length === 1) strict = true; + + if (this.length !== array.length) return false; + + for (let i = 0; i < this.length; i++) { + if (this[i] instanceof Array && array[i] instanceof Array) { + if (!this[i].equals(array[i], strict)) return false; + } else if (strict && this[i] !== array[i]) { + return false; + } else if (!strict) { + return this.sort().equals(array.sort(), true); + } + } + return true; + }; + } + if ([].lastElement === undefined) { + // eslint-disable-next-line no-extend-native + Array.prototype.lastElement = function () { + let instanceCheck = this; + if (instanceCheck === undefined) { + return undefined; + } else { + let instance = instanceCheck as []; + return instance[instance.length - 1]; + } + }; + } +}; diff --git a/ui/src/core/extensions/extensions.ts b/ui/src/core/extensions/extensions.ts new file mode 100644 index 0000000..e4111fd --- /dev/null +++ b/ui/src/core/extensions/extensions.ts @@ -0,0 +1,6 @@ +import { ArrayExtensions } from "./array"; + +export const extensions = () =>{ + ArrayExtensions() +} + \ No newline at end of file diff --git a/ui/src/core/helper/result.ts b/ui/src/core/helper/result.ts new file mode 100644 index 0000000..bdfc37c --- /dev/null +++ b/ui/src/core/helper/result.ts @@ -0,0 +1,535 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */ +/* eslint-disable @typescript-eslint/no-namespace */ + + +function isAsyncFn(fn: Function) { + return fn.constructor.name === "AsyncFunction"; +} + +function isResult(value: unknown): value is Result { + return value instanceof Ok || value instanceof Err; +} + +interface SyncThenable { + isSync: true; + then Promise>(cb: Fn): ReturnType; + then any>(cb: Fn): SyncThenable; +} + + +function syncThenable(): SyncThenable { + function then Promise>(cb: Fn): ReturnType; + function then any>(cb: Fn): SyncThenable; + function then(cb: any) { + const result = cb(); + if (result instanceof Promise) { + return result; + } + + return syncThenable(); + } + + return { + isSync: true, + then, + }; +} + +function forEachValueThunkOrPromise( + items: unknown[], + execFn: (value: T) => boolean, + foldFn: () => unknown +) { + let shouldBreak = false; + + const result: any = items.reduce((prev: { then: Function }, valueOrThunk) => { + return prev.then(() => { + if (shouldBreak) { + return null; + } + + function run(value: T) { + const isSuccess = execFn(value); + if (!isSuccess) { + shouldBreak = true; + } + } + + const valueOrPromise = + typeof valueOrThunk === "function" ? valueOrThunk() : valueOrThunk; + + if (valueOrPromise instanceof Promise) { + return valueOrPromise.then(run); + } + + return run(valueOrPromise); + }); + }, syncThenable()); + + if ((result as SyncThenable).isSync) { + return foldFn(); + } + + return result.then(() => { + return foldFn(); + }); +} + +export type Result< + ErrorType, + OkType, + RollbackFn extends RollbackFunction = any +> = Ok | Err; + +interface IResult { + isSuccess(): this is Ok; + + isFailure(): this is Err; + + getOrNull(): OkType | null; + + toString(): string; + + inspect(): string; + + fold( + onSuccess: (value: OkType) => R, + onFailure: (error: ErrorType) => R + ): R; + fold( + onSuccess: (value: OkType) => Promise, + onFailure: (error: ErrorType) => Promise + ): Promise; + + getOrDefault(defaultValue: OkType): OkType; + + getOrElse(onFailure: (error: ErrorType) => OkType): OkType; + getOrElse(onFailure: (error: ErrorType) => Promise): Promise; + + getOrThrow(): OkType; + + map( + fn: (value: OkType) => Promise + ): Promise< + JoinErrorTypes< + ErrorType, + T extends Result ? T : Result + > + >; + map( + fn: (value: OkType) => T + ): JoinErrorTypes< + ErrorType, + T extends Result ? T : Result + >; + + rollback(): Result | Promise>; +} + +type InferErrorType> = T extends Result< + infer Errortype, + any, + any +> + ? Errortype + : never; + +type InferOkType> = T extends Result< + any, + infer OkType, + any +> + ? OkType + : never; + +type JoinErrorTypes> = Result< + ErrorType | InferErrorType, + InferOkType, + any +>; + +type ExtractErrorTypes = { + [Index in keyof Tuple]: Tuple[Index] extends Result + ? InferErrorType + : never; +}[number]; + +type MapResultTupleToOkTypeTuple = { + [Index in keyof Tuple]: Tuple[Index] extends Result + ? InferOkType + : never; +}; + +type RollbackFunction = (() => void) | (() => Promise); + +type HasAsyncRollbackFunction = { + [Index in keyof T]: T[Index] extends () => Promise | infer U + ? U extends Result Promise> + ? true + : false + : false; +}[number] extends false + ? false + : true; + +type UnwrapThunks = { + [Index in keyof T]: T[Index] extends () => Promise + ? U + : T[Index] extends () => infer U + ? U + : T[Index]; +}; + +type HasAsyncThunk = { + [Index in keyof T]: T[Index] extends () => Promise ? true : false; +}[number] extends false + ? false + : true; + +type PromiseReturnType any> = T extends ( + ...args: any +) => Promise + ? U + : never; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export namespace Result { + export function ok< + ErrorType extends unknown, + OkType, + RollbackFn extends RollbackFunction = any + >( + value?: OkType, + rollbackFn?: RollbackFn + ): Result { + return new Ok( + value || null!, + rollbackFn + ) as any; + } + + export function error< + ErrorType extends unknown, + OkType extends unknown, + RollbackFn extends RollbackFunction = any + >( + error: ErrorType, + rollbackFn?: RollbackFn + ): Result { + return new Err(error, rollbackFn); + } + + type SafeReturnType = T extends Result + ? Result, InferOkType, never> + : Result; + + export function safe( + fn: () => Promise + ): Promise>; + export function safe(fn: () => T): SafeReturnType; + export function safe( + err: ErrorType | (new (...args: any[]) => ErrorType), + fn: () => Promise + ): Promise>; + export function safe( + err: ErrorType | (new (...args: any[]) => ErrorType), + fn: () => T + ): SafeReturnType; + export function safe(errOrFn: any, fn?: any) { + const hasCustomError = fn !== undefined; + + const execute = hasCustomError ? fn : errOrFn; + + function getError(caughtError: Error) { + if (!hasCustomError) { + return caughtError; + } + + if (typeof errOrFn === "function") { + return new errOrFn(caughtError); + } + + return errOrFn; + } + + try { + const resultOrPromise = execute(); + + if (resultOrPromise instanceof Promise) { + return resultOrPromise + .then((okValue) => { + return isResult(okValue) ? okValue : Result.ok(okValue); + }) + .catch((caughtError) => error(getError(caughtError))); + } + + return isResult(resultOrPromise) + ? resultOrPromise + : Result.ok(resultOrPromise); + } catch (caughtError) { + return error(getError(caughtError as Error)); + } + } + + type CombineResult< + T extends (unknown | (() => unknown) | (() => Promise))[] + > = Result< + ExtractErrorTypes>, + MapResultTupleToOkTypeTuple>, + HasAsyncRollbackFunction extends true ? () => Promise : () => void + >; + + export function combine< + T extends (unknown | (() => unknown) | (() => Promise))[] + >( + ...items: T + ): HasAsyncThunk extends true + ? Promise> + : CombineResult { + if (!items.length) { + throw new Error("Expected at least 1 argument"); + } + + const values: unknown[] = []; + const rollbacks: RollbackFunction[] = []; + let error: Err | null = null; + + function rollback() { + const reversedRollbacks = rollbacks.reverse(); + const wrappedRollbackFns = reversedRollbacks.map((fn) => Result.wrap(fn)); + + let error: Err | null = null; + + return forEachValueThunkOrPromise( + wrappedRollbackFns, + (result: Result) => { + if (result.isFailure()) { + error = Result.error(result.error) as any; + return false; + } + + return true; + }, + () => error || ok() + ); + } + + return forEachValueThunkOrPromise( + items, + (result: Result) => { + if (result.isFailure()) { + error = Result.error(result.error, rollback) as any; + return false; + } + + values.push(result.value); + rollbacks.push(() => result.rollback()); + return true; + }, + () => error || ok(values, rollback) + ); + } + + export function wrap Promise>( + fn: Fn + ): ( + ...args: Parameters + ) => Promise, never>>; + export function wrap any>( + fn: Fn + ): (...args: Parameters) => Result, never>; + export function wrap(fn: any) { + return function wrapped(...args: any) { + try { + const resultOrPromise = fn(...args); + + if (resultOrPromise instanceof Promise) { + return resultOrPromise + .then((okValue) => Result.ok(okValue)) + .catch((err) => error(err)); + } + + return ok(resultOrPromise); + } catch (err) { + return error(err); + } + }; + } +} + +abstract class Base< + ErrorType extends unknown, + OkType extends unknown, + RollbackFn extends RollbackFunction +> implements IResult +{ + constructor(protected readonly rollbackFn?: RollbackFn) {} + + errorOrNull(): ErrorType | null { + if (this.isSuccess()) { + return null; + } + + return (this as any).error as ErrorType; + } + + getOrNull(): OkType | null { + if (this.isFailure()) { + return null; + } + + return (this as any).value as OkType; + } + + toString(): string { + throw new Error("Method not implemented."); + } + inspect(): string { + return this.toString(); + } + + fold( + onSuccess: (value: OkType) => R, + onFailure: (error: ErrorType) => R + ): R; + + fold( + onSuccess: (value: OkType) => Promise, + onFailure: (error: ErrorType) => Promise + ): Promise; + fold(onSuccess: any, onFailure: any) { + if (this.isFailure()) { + return onFailure(this.error); + } + + return onSuccess((this as any).value as OkType); + } + + getOrDefault(defaultValue: OkType): OkType { + if (this.isSuccess()) { + return this.value; + } + + return defaultValue; + } + + getOrElse(onFailure: (error: ErrorType) => OkType): OkType; + getOrElse(onFailure: (error: ErrorType) => Promise): Promise; + getOrElse(onFailure: any) { + if (this.isSuccess()) { + return isAsyncFn(onFailure) ? Promise.resolve(this.value) : this.value; + } + + return onFailure((this as any).error as ErrorType); + } + + getOrThrow(): OkType { + if (this.isFailure()) { + throw this.error; + } + + return (this as any).value as OkType; + } + + isSuccess(): this is Ok { + throw new Error("Method not implemented."); + } + isFailure(): this is Err { + throw new Error("Method not implemented."); + } + + map( + fn: (value: OkType) => Promise + ): Promise< + JoinErrorTypes< + ErrorType, + T extends Result ? T : Result + > + >; + map( + fn: (value: OkType) => T + ): JoinErrorTypes< + ErrorType, + T extends Result ? T : Result + >; + map(fn: any) { + if (this.isFailure()) { + return isAsyncFn(fn) ? Promise.resolve(this) : this; + } + + const result = Result.safe(() => fn((this as any).value) as any); + + return result as any; + } + + rollback(): RollbackFn extends RollbackFunction + ? RollbackFn extends () => Promise + ? Promise> + : Result + : void { + if (this.rollbackFn) { + return this.rollbackFn() as any; + } + + return null as any; + } +} + +class Ok< + ErrorType extends unknown, + OkType extends unknown, + RollbackFn extends RollbackFunction +> extends Base { + public readonly value: OkType; + + constructor(val: OkType, rollbackFn?: RollbackFn) { + super(rollbackFn); + this.value = val; + } + + isSuccess(): this is Ok { + return true; + } + + isFailure(): this is Err { + return false; + } + + toString(): string { + return `Result.Ok(${this.value})`; + } + + forward(): Result { + return Result.ok(this.value); + } +} + +class Err< + ErrorType extends unknown, + OkType extends unknown, + RollbackFn extends RollbackFunction +> extends Base { + public readonly error: ErrorType; + + constructor(err: ErrorType, rollbackFn?: RollbackFn) { + super(rollbackFn); + this.error = err; + } + + isSuccess(): this is Ok { + return false; + } + + isFailure(): this is Err { + return true; + } + + toString(): string { + return `Result.Error(${this.error})`; + } + + forward(): Result { + return Result.error(this.error); + } +} diff --git a/ui/src/core/helper/validate.ts b/ui/src/core/helper/validate.ts new file mode 100644 index 0000000..f570767 --- /dev/null +++ b/ui/src/core/helper/validate.ts @@ -0,0 +1,3 @@ +export function validateRequired(value: string) { + return value ? undefined : "required"; +} diff --git a/ui/src/core/l10n/i18n.ts b/ui/src/core/l10n/i18n.ts new file mode 100644 index 0000000..5251bfe --- /dev/null +++ b/ui/src/core/l10n/i18n.ts @@ -0,0 +1,15 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import { resources } from "./locale"; +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + resources, + lng: "en", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // if you're using a language detector, do not define the lng option + + interpolation: { + escapeValue: false, // react already safes from xss + }, + }); diff --git a/ui/src/core/l10n/locale.ts b/ui/src/core/l10n/locale.ts new file mode 100644 index 0000000..9172023 --- /dev/null +++ b/ui/src/core/l10n/locale.ts @@ -0,0 +1,12 @@ +export const resources = { + en: { + translation: { + "Welcome to React": "Welcome to React and react-i18next", + }, + }, + fr: { + translation: { + "Welcome to React": "Bienvenue à React et react-i18next", + }, + }, +}; diff --git a/ui/src/features/trigger/model/trigger_model.ts b/ui/src/core/model/trigger_model.ts similarity index 87% rename from ui/src/features/trigger/model/trigger_model.ts rename to ui/src/core/model/trigger_model.ts index 356e12e..f3411be 100644 --- a/ui/src/features/trigger/model/trigger_model.ts +++ b/ui/src/core/model/trigger_model.ts @@ -1,6 +1,7 @@ export interface ITriggerModel { _id?: string; type: string; + description:string; value: string[]; } diff --git a/ui/src/core/repository/http_repository.ts b/ui/src/core/repository/http_repository.ts index 555e0f1..eab857b 100644 --- a/ui/src/core/repository/http_repository.ts +++ b/ui/src/core/repository/http_repository.ts @@ -5,7 +5,7 @@ export enum HttpMethod { export class HttpRepository { - private server = 'http://localhost:3000' + private server = 'http://localhost:4001' public async jsonRequest(method: HttpMethod, url: string, data?: any): Promise { const reqInit = { diff --git a/ui/src/core/repository/socket_repository.ts b/ui/src/core/repository/socket_repository.ts new file mode 100644 index 0000000..88f7e9f --- /dev/null +++ b/ui/src/core/repository/socket_repository.ts @@ -0,0 +1,14 @@ +import { Socket, io } from "socket.io-client"; + +export class SocketRepository { + serverURL = "ws://localhost:4001"; + socket: Socket | undefined; + async connect() { + const socket = io(this.serverURL); + this.socket = socket; + socket.connect(); + socket.on('mock', (d) =>{ + console.log(d) + }) + } +} diff --git a/ui/src/core/routers/routers.tsx b/ui/src/core/routers/routers.tsx new file mode 100644 index 0000000..dc6add1 --- /dev/null +++ b/ui/src/core/routers/routers.tsx @@ -0,0 +1,36 @@ +import { createBrowserRouter } from "react-router-dom"; +import { + AllProjectScreen, + AllProjectScreenPath, +} from "../../features/all_projects/presentation/all_projects_screen"; +import { + PipelineInstanceScreen, + PipelineScreenPath, +} from "../../features/pipeline_instance_main_screen/pipeline_instance_screen"; +import { + SelectProjectScreen, + SelectProjectScreenPath, +} from "../../features/select_project/presentation/select_project"; +import { + CreatePipelineScreen, + CreatePipelineScreenPath, +} from "../../features/create_pipeline/presentation/create_pipeline_screen"; + +export const router = createBrowserRouter([ + { + path: AllProjectScreenPath, + element: , + }, + { + path: PipelineScreenPath, + element: , + }, + { + path: SelectProjectScreenPath, + element: , + }, + { + path: CreatePipelineScreenPath, + element: , + }, +]); diff --git a/ui/src/core/ui/header/header.tsx b/ui/src/core/ui/header/header.tsx new file mode 100644 index 0000000..83f6e11 --- /dev/null +++ b/ui/src/core/ui/header/header.tsx @@ -0,0 +1,94 @@ +import * as React from "react"; +import { Typography } from "antd"; +import { Col, Row } from "antd"; +import { LinkTypography } from "../link/link"; +import { ReactComponent as LeftIcon } from "../../assets/icons/left_icon.svg"; +import { useNavigate } from "react-router-dom"; + +const { Title } = Typography; + +export interface IHeader { + largeText: string; + minText?: string; + path?: string; + needBackButton?: undefined | any; +} + +export const Header: React.FunctionComponent = (props: IHeader) => { + const navigate = useNavigate(); + const needBackButton = props.needBackButton !== undefined ? false : true; + + return ( + + + {needBackButton ? ( + <> +
{ + navigate(-1); + }} + style={{ + position: "absolute", + zIndex: 1, + left: "10px", + backgroundColor: "#456BD9", + border: "0.1875em solid #0F1C3F", + borderRadius: "50%", + height: "60px", + width: "60px", + cursor: "pointer", + }} + > + +
+ + {props.largeText} + + + ) : ( + <> + + {props.largeText} + + + )} +
+ + {props.minText !== undefined ? ( + + ) : ( + <> + )} + + ); +}; diff --git a/ui/src/core/ui/link/link.tsx b/ui/src/core/ui/link/link.tsx new file mode 100644 index 0000000..05e3bf3 --- /dev/null +++ b/ui/src/core/ui/link/link.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { Typography } from "antd"; +import { useNavigate } from "react-router-dom"; + +const { Link } = Typography; + +export interface ILinkTypography { + path: string; + text: string; + style?: React.CSSProperties; +} + +export const LinkTypography: React.FunctionComponent = ( + props: ILinkTypography +) => { + const navigate = useNavigate(); + + return ( + { + navigate(props.path); + }} + > + {props.text} + + ); +}; diff --git a/ui/src/core/ui/list/list.tsx b/ui/src/core/ui/list/list.tsx new file mode 100644 index 0000000..9861cdb --- /dev/null +++ b/ui/src/core/ui/list/list.tsx @@ -0,0 +1,66 @@ +import { Row } from "antd"; +import { ReactComponent as AddIcon } from "../../assets/icons/add.svg"; +import { observer } from "mobx-react-lite"; + +export type CallBackFunction = (a: string) => void; + +export interface ListElement { + text: string; + color?: string; +} + +export interface IPropsList { + values: ListElement[]; + headers?: string; + onClick?: CallBackFunction; +} + +export const List: React.FunctionComponent = observer( + (props) => { + return ( +
+ {props.headers !== undefined ? <>{props.headers} : <>} + {props.values.map((el) => { + return ( + +
+ +
+ {el.text} +
+
+ { + if (props.onClick !== undefined) { + props.onClick(el.text); + } + }} + /> + + ); + })} +
+ ); + } +); diff --git a/ui/src/core/ui/loader/loader.css b/ui/src/core/ui/loader/loader.css new file mode 100644 index 0000000..6239784 --- /dev/null +++ b/ui/src/core/ui/loader/loader.css @@ -0,0 +1,31 @@ +.loader { + position: relative; + width: 64px; + height: 64px; + background-color: rgba(0, 0, 0, 0.5); + transform: rotate(45deg); + overflow: hidden; +} +.loader:after{ + content: ''; + position: absolute; + inset: 8px; + margin: auto; + background: #222b32; +} +.loader:before{ + content: ''; + position: absolute; + inset: -15px; + margin: auto; + background: #de3500; + animation: diamondLoader 2s linear infinite; +} +@keyframes diamondLoader { + 0% ,10% { + transform: translate(-64px , -64px) rotate(-45deg) + } + 90% , 100% { + transform: translate(0px , 0px) rotate(-45deg) + } +} \ No newline at end of file diff --git a/ui/src/core/ui/loader/loader.tsx b/ui/src/core/ui/loader/loader.tsx new file mode 100644 index 0000000..79e0f01 --- /dev/null +++ b/ui/src/core/ui/loader/loader.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import "./loader.css"; + +export const Loader: React.FunctionComponent = () => { + return ( +
+
+
+ ); +}; diff --git a/ui/src/core/ui/pages/load_page.tsx b/ui/src/core/ui/pages/load_page.tsx new file mode 100644 index 0000000..a6d3c4c --- /dev/null +++ b/ui/src/core/ui/pages/load_page.tsx @@ -0,0 +1,49 @@ +import * as React from "react"; +import { Header, IHeader } from "../header/header"; +import { Loader } from "../loader/loader"; +import { ReactComponent as ErrorIcon } from "../../assets/icons/error.svg"; +import { Typography } from "antd"; +import { observer } from "mobx-react-lite"; +const { Title } = Typography; + +interface ILoadPage extends IHeader { + isLoading: boolean; + isError: boolean; + children?: JSX.Element | JSX.Element[]; +} + +export const LoadPage: React.FunctionComponent = observer(( + props: ILoadPage +) => { + return ( + <> +
+ {props.isError ? ( + <> + + + not expected error + + + ) : ( + <> + )} + {props.isLoading ? ( +
+ +
+ ) : ( + <>{props.children} + )} + + ); +}); diff --git a/ui/src/features/all_projects/data/project_repository.ts b/ui/src/features/all_projects/data/project_repository.ts new file mode 100644 index 0000000..247538d --- /dev/null +++ b/ui/src/features/all_projects/data/project_repository.ts @@ -0,0 +1,3 @@ +import { HttpRepository } from "../../../core/repository/http_repository"; + +export class ProjectRepository extends HttpRepository {} diff --git a/ui/src/features/all_projects/presentation/all_projects_screen.tsx b/ui/src/features/all_projects/presentation/all_projects_screen.tsx new file mode 100644 index 0000000..9032667 --- /dev/null +++ b/ui/src/features/all_projects/presentation/all_projects_screen.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import { SelectProjectScreenPath } from "../../select_project/presentation/select_project"; +import { Header } from "../../../core/ui/header/header"; + +export const AllProjectScreenPath = "/"; + +export const AllProjectScreen: React.FunctionComponent = () => { + return ( + <> +
+ + ); +}; diff --git a/ui/src/features/all_projects/presentation/all_projects_store.ts b/ui/src/features/all_projects/presentation/all_projects_store.ts new file mode 100644 index 0000000..c649338 --- /dev/null +++ b/ui/src/features/all_projects/presentation/all_projects_store.ts @@ -0,0 +1,12 @@ +import { makeAutoObservable } from "mobx"; +import { ProjectRepository } from "../data/project_repository"; + +class AllProjectStore { + constructor(repository: ProjectRepository) { + + makeAutoObservable(this); + } + +} + +export const allProjectStore = new AllProjectStore(new ProjectRepository()); \ No newline at end of file diff --git a/ui/src/features/create_pipeline/data/create_pipeline_repository.ts b/ui/src/features/create_pipeline/data/create_pipeline_repository.ts new file mode 100644 index 0000000..99e595e --- /dev/null +++ b/ui/src/features/create_pipeline/data/create_pipeline_repository.ts @@ -0,0 +1,29 @@ +import { + HttpMethod, + HttpRepository, +} from "../../../core/repository/http_repository"; +import { ITriggerModel } from "../../../core/model/trigger_model"; +import { Result } from "../../../core/helper/result"; +import { IProcess } from "../../create_process/model/process_model"; + +export class CreatePipelineRepository extends HttpRepository { + async getTriggers(page = 1): Promise> { + try { + return Result.ok( + await this.jsonRequest(HttpMethod.GET, `/trigger?${page}`) + ); + } catch (error) { + return Result.error(error as Error); + } + } + + async getProcessed(page = 1): Promise> { + try { + return Result.ok( + await this.jsonRequest(HttpMethod.GET, `/process?${page}`) + ); + } catch (error) { + return Result.error(error as Error); + } + } +} diff --git a/ui/src/features/create_pipeline/model/pipiline_model.ts b/ui/src/features/create_pipeline/model/pipiline_model.ts new file mode 100644 index 0000000..c87f201 --- /dev/null +++ b/ui/src/features/create_pipeline/model/pipiline_model.ts @@ -0,0 +1,3 @@ +export interface IColor { + color:string; +} \ No newline at end of file diff --git a/ui/src/features/create_pipeline/presentation/create_pipeline_screen.tsx b/ui/src/features/create_pipeline/presentation/create_pipeline_screen.tsx new file mode 100644 index 0000000..cdfce17 --- /dev/null +++ b/ui/src/features/create_pipeline/presentation/create_pipeline_screen.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { Row, Input, Button } from "antd"; +import { LoadPage } from "../../../core/ui/pages/load_page"; +import { createPipelineStore } from "./create_pipeline_store"; +import { observer } from "mobx-react-lite"; +import { List } from "../../../core/ui/list/list"; + +export const CreatePipelineScreenPath = "/create_pipeline"; + +export const CreatePipelineScreen: React.FunctionComponent = observer(() => { + return ( + <> + + + { + return { text: el.description }; + })} + onClick={(e) => createPipelineStore.addProcess(e)} + /> +
+ + + +
+ + { + return { text: el.description }; + })} + onClick={(e) => createPipelineStore.addTrigger(e)} + /> +
+ + } + /> + + ); +}); diff --git a/ui/src/features/create_pipeline/presentation/create_pipeline_store.ts b/ui/src/features/create_pipeline/presentation/create_pipeline_store.ts new file mode 100644 index 0000000..3e00f6a --- /dev/null +++ b/ui/src/features/create_pipeline/presentation/create_pipeline_store.ts @@ -0,0 +1,95 @@ +import { makeAutoObservable } from "mobx"; +import { CreatePipelineRepository } from "../data/create_pipeline_repository"; +import { ITriggerModel } from "../../../core/model/trigger_model"; +import { IProcess } from "../../create_process/model/process_model"; +// TODO:()rename + +enum Direction { + PROCESS, + TRIGGER, +} +interface CommonView { + text: string; + color: string; + type: Direction; +} +export class CreatePipelineStore { + repository: CreatePipelineRepository; + triggersModels: ITriggerModel[] = []; + processModels: IProcess[] = []; + pipelineViewModel: CommonView[] = []; + + isLoading = false; + isError = false; + page = 1; + + constructor(repository: CreatePipelineRepository) { + this.repository = repository; + makeAutoObservable(this); + this.loadTriggers(); + this.loadProcess(); + } + + addProcess(e: string): void { + const lastElement = this.pipelineViewModel.lastElement() + + if(lastElement !== undefined){ + if(lastElement.type !== Direction.TRIGGER){ + // need UI say + return + } + } + + this.pipelineViewModel.push({ + text: e, + color: "activeborder", + type: Direction.PROCESS, + }); + } + + addTrigger(e: string): void { + const lastElement = this.pipelineViewModel.lastElement() + + if(lastElement !== undefined){ + if(lastElement.type !== Direction.PROCESS){ + // need UI say + return + } + } + this.pipelineViewModel.push({ + text: e, + color: "blanchedalmond", + type: Direction.TRIGGER, + }); + } + createPipeline(): void {} + async loadProcess() { + this.isLoading = true; + const result = await this.repository.getProcessed(); + result.fold( + (s) => { + this.processModels = s; + }, + (_e) => { + this.isError = true; + } + ); + this.isLoading = false; + } + async loadTriggers() { + this.isLoading = true; + const result = await this.repository.getTriggers(this.page); + result.fold( + (s) => { + this.triggersModels = s; + }, + (_e) => { + this.isError = true; + } + ); + this.isLoading = false; + } +} +export const createPipelineStore = new CreatePipelineStore( + new CreatePipelineRepository() +); diff --git a/ui/src/features/create_process/data/process_repostiory.ts b/ui/src/features/create_process/data/process_repostiory.ts new file mode 100644 index 0000000..fa4e44b --- /dev/null +++ b/ui/src/features/create_process/data/process_repostiory.ts @@ -0,0 +1,11 @@ +import { + HttpMethod, + HttpRepository, +} from "../../../core/repository/http_repository"; +import { IProcess } from "../model/process_model"; + +export class ProcessRepository extends HttpRepository { + async save(model: IProcess): Promise { + await this.jsonRequest(HttpMethod.POST, "/process", model); + } +} diff --git a/ui/src/features/create_process/model/process_model.ts b/ui/src/features/create_process/model/process_model.ts new file mode 100644 index 0000000..fb4be3e --- /dev/null +++ b/ui/src/features/create_process/model/process_model.ts @@ -0,0 +1,26 @@ +export interface IProcess { + description: string; + type: EXEC_TYPE | string; + command: string; + isGenerating: boolean; + isLocaleCode: boolean; + issueType: IssueType | string; + timeout?: number; + commit?: string | undefined; +} +export enum EXEC_TYPE { + SPAWN = "SPAWN", + EXEC = "EXEC", +} +export enum IssueType { + WARNING = "WARNING", + ERROR = "ERROR", +} +export const processModelMock: IProcess = { + description: "", + type: EXEC_TYPE.SPAWN, + command: "", + isGenerating: true, + isLocaleCode: true, + issueType: IssueType.WARNING, +}; diff --git a/ui/src/features/create_process/presentation/create_process_screen.tsx b/ui/src/features/create_process/presentation/create_process_screen.tsx new file mode 100644 index 0000000..a660268 --- /dev/null +++ b/ui/src/features/create_process/presentation/create_process_screen.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; +import { processStore } from "./logic/process_store"; +import { observer } from "mobx-react-lite"; +import { + SubmitButton, + Input, + ResetButton, + Form, + Radio, + Switch, +} from "formik-antd"; +import { Formik } from "formik"; +import { Row, Col } from "antd"; +import { EXEC_TYPE, IssueType, processModelMock } from "../model/process_model"; + +export const CreateProcessScreen = observer(() => { + return ( +
+
+ { + await processStore.saveResult(values); + actions.setSubmitting(false); + actions.resetForm(); + }} + validate={(values) => { + if (!values.command) { + return { command: "required" }; + } + if (!values.description) { + return { description: "required" }; + } + return {}; + }} + render={() => ( +
+
+ + + + + + + + + + + + + + + + Reset + Submit + + +
+
+ )} + /> +
+
+ ); +}); diff --git a/ui/src/features/create_process/presentation/logic/process_store.ts b/ui/src/features/create_process/presentation/logic/process_store.ts new file mode 100644 index 0000000..d31299a --- /dev/null +++ b/ui/src/features/create_process/presentation/logic/process_store.ts @@ -0,0 +1,17 @@ +import { makeAutoObservable } from "mobx"; +import { v4 as uuidv4 } from "uuid"; +import { ProcessRepository } from "../../data/process_repostiory"; +import { IProcess } from "../../model/process_model"; + +class ProcessStore { + repository: ProcessRepository; + constructor(repository: ProcessRepository) { + this.repository = repository; + makeAutoObservable(this); + } + async saveResult(model:IProcess) { + await this.repository.save(model) + } +} + +export const processStore = new ProcessStore(new ProcessRepository()); diff --git a/ui/src/features/trigger/data/trigger_repository.ts b/ui/src/features/create_trigger/data/trigger_repository.ts similarity index 80% rename from ui/src/features/trigger/data/trigger_repository.ts rename to ui/src/features/create_trigger/data/trigger_repository.ts index a940917..3127310 100644 --- a/ui/src/features/trigger/data/trigger_repository.ts +++ b/ui/src/features/create_trigger/data/trigger_repository.ts @@ -2,7 +2,7 @@ import { HttpMethod, HttpRepository, } from "../../../core/repository/http_repository"; -import { ITriggerModel } from "../model/trigger_model"; +import { ITriggerModel } from "../../../core/model/trigger_model"; export class TriggerRepository extends HttpRepository { public async save(model: ITriggerModel) { diff --git a/ui/src/features/trigger/model/trigger_form_view_model.ts b/ui/src/features/create_trigger/model/trigger_form_view_model.ts similarity index 71% rename from ui/src/features/trigger/model/trigger_form_view_model.ts rename to ui/src/features/create_trigger/model/trigger_form_view_model.ts index a9623a5..9ffe677 100644 --- a/ui/src/features/trigger/model/trigger_form_view_model.ts +++ b/ui/src/features/create_trigger/model/trigger_form_view_model.ts @@ -1,4 +1,4 @@ -import { TriggerType } from "./trigger_model"; +import { TriggerType } from "../../../core/model/trigger_model"; export interface ITriggerViewValueModel { value: string; diff --git a/ui/src/features/trigger/presentation/components/code_trigger_form.tsx b/ui/src/features/create_trigger/presentation/components/code_trigger_form.tsx similarity index 83% rename from ui/src/features/trigger/presentation/components/code_trigger_form.tsx rename to ui/src/features/create_trigger/presentation/components/code_trigger_form.tsx index fe1f528..14e7946 100644 --- a/ui/src/features/trigger/presentation/components/code_trigger_form.tsx +++ b/ui/src/features/create_trigger/presentation/components/code_trigger_form.tsx @@ -2,13 +2,13 @@ import * as React from "react"; import Editor from "@monaco-editor/react"; import { Button } from "antd"; import { observer } from "mobx-react-lite"; -import { triggerStore } from "../logic/trigger_store"; +import { triggerStore } from "../trigger_store"; export const CodeTriggerForm: React.FunctionComponent = observer(() => { return ( <>
- + { onChange={(v) => { triggerStore.writeNewTrigger(v); }} - onValidate={(m) => { - console.log(m); - }} + onValidate={(_m) => {}} /> - +
-
- +
+ + + ); +}; diff --git a/ui/src/features/select_project/data/select_project_repository.ts b/ui/src/features/select_project/data/select_project_repository.ts new file mode 100644 index 0000000..84e27d5 --- /dev/null +++ b/ui/src/features/select_project/data/select_project_repository.ts @@ -0,0 +1,18 @@ +import { Result } from "../../../core/helper/result"; +import { + HttpMethod, + HttpRepository, +} from "../../../core/repository/http_repository"; +import { IProjectModel } from "../model/project_model"; + +export class SelectProjectRepository extends HttpRepository { + async getAllProjects(page = 1): Promise> { + try { + return Result.ok( + await this.jsonRequest(HttpMethod.GET, `/project?${page}`) + ); + } catch (error) { + return Result.error(error as Error); + } + } +} diff --git a/ui/src/features/select_project/model/project_model.ts b/ui/src/features/select_project/model/project_model.ts new file mode 100644 index 0000000..056d1d8 --- /dev/null +++ b/ui/src/features/select_project/model/project_model.ts @@ -0,0 +1,8 @@ +import { IProcess } from "../../create_process/model/process_model"; + +export interface IProjectModel { + pipelines: [IProcess]; + rootDir: string; + description: string; +} + \ No newline at end of file diff --git a/ui/src/features/select_project/presentation/select_project.tsx b/ui/src/features/select_project/presentation/select_project.tsx new file mode 100644 index 0000000..33db1f8 --- /dev/null +++ b/ui/src/features/select_project/presentation/select_project.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; +import { selectProjectStore } from "./select_project_store"; +import { Loader } from "../../../core/ui/loader/loader"; +import { observer } from "mobx-react-lite"; +import { Header } from "../../../core/ui/header/header"; +import { CreatePipelineScreenPath } from "../../create_pipeline/presentation/create_pipeline_screen"; +import { LoadPage } from "../../../core/ui/pages/load_page"; + +export const SelectProjectScreenPath = "/select_project"; + +export const SelectProjectScreen: React.FunctionComponent = observer(() => { + return ( + <> + { + return ( + <> +
{el.description}
+
+(РЕАЛИЗУЙ ТУТ ПЛЮСИК БЛЯТЬ ИЛИ КНОПКУ)
+ + ); + })} + /> + + ); +}); diff --git a/ui/src/features/select_project/presentation/select_project_store.ts b/ui/src/features/select_project/presentation/select_project_store.ts new file mode 100644 index 0000000..1be8ef1 --- /dev/null +++ b/ui/src/features/select_project/presentation/select_project_store.ts @@ -0,0 +1,35 @@ +import { makeAutoObservable } from "mobx"; +import { SelectProjectRepository } from "../data/select_project_repository"; +import { IProjectModel } from "../model/project_model"; + +class SelectProjectStore { + repository: SelectProjectRepository; + isLoading = false; + isError = false; + page = 1; + projects: IProjectModel[] = []; + + constructor(repository: SelectProjectRepository) { + this.repository = repository; + makeAutoObservable(this); + this.getPipelines(); + } + + async getPipelines(): Promise { + this.isLoading = true; + const result = await this.repository.getAllProjects(this.page); + result.fold( + (s) => { + this.projects = s; + }, + (_e) => { + this.isError = true; + } + ); + this.isLoading = false; + } +} + +export const selectProjectStore = new SelectProjectStore( + new SelectProjectRepository() +); diff --git a/ui/src/features/socket_lister/socket_lister.tsx b/ui/src/features/socket_lister/socket_lister.tsx new file mode 100644 index 0000000..42d2e83 --- /dev/null +++ b/ui/src/features/socket_lister/socket_lister.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { ReactComponent as ReloadIcon } from "../../core/assets/icons/reload.svg"; +import { socketListerStore } from "./socket_lister_store"; +import { observer } from "mobx-react-lite"; + +export interface ISocketListerProps { + children?: JSX.Element; +} + +export const SocketLister = observer((props: ISocketListerProps) => { + return ( + <> + {socketListerStore.socketDisconnect ? ( + { + socketListerStore.reconnect(); + }} + style={{ + height: "70px", + backgroundColor: "silver", + width: "-webkit-fill-available", + padding: "10px", + cursor: "pointer", + }} + /> + ) : ( + <> + )} + + {props.children} + + ); +}); diff --git a/ui/src/features/socket_lister/socket_lister_store.ts b/ui/src/features/socket_lister/socket_lister_store.ts new file mode 100644 index 0000000..f2d6b9a --- /dev/null +++ b/ui/src/features/socket_lister/socket_lister_store.ts @@ -0,0 +1,18 @@ +import { makeAutoObservable } from "mobx"; +import { SocketRepository } from "../../core/repository/socket_repository"; + +class SocketListerStore { + repository: SocketRepository; + socketDisconnect = false; + + constructor(repository: SocketRepository) { + this.repository = repository; + makeAutoObservable(this); + } + async reconnect() { + await this.repository.connect() + this.socketDisconnect = false + } +} + +export const socketListerStore = new SocketListerStore(new SocketRepository()); diff --git a/ui/src/index.tsx b/ui/src/index.tsx index e52915e..06966ef 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -1,16 +1,22 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; +import React from "react"; +import ReactDOM from "react-dom/client"; - -import { TriggerScreen } from './features/trigger/presentation/trigger_presentation'; -import "antd/dist/antd.css"; +import "antd/dist/antd.min.css"; +import { RouterProvider } from "react-router-dom"; +import { router } from "./core/routers/routers"; +import { SocketLister } from "./features/socket_lister/socket_lister"; +import { extensions } from "./core/extensions/extensions"; + +extensions(); const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById("root") as HTMLElement ); root.render( - - - + <> + + + + ); diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 092e6ad..397b455 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -21,7 +17,5 @@ "jsx": "react-jsx", "useDefineForClassFields": true }, - "include": [ - "src" - ] + "include": ["src"] }