diff --git a/.vscode/settings.json b/.vscode/settings.json index da63cc5..30e0524 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,6 @@ "*ui": false, "*ui.*": false }, - "cSpell.words": ["antd", "fileupload", "metadatas", "undici", "uuidv"], + "cSpell.words": ["antd", "Collada", "Contolls", "fileupload", "metadatas", "undici", "uuidv"], "editor.rulers": [100] } diff --git a/server/.gitignore b/server/.gitignore index 10894c2..b8422f0 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -9,3 +9,4 @@ package-lock.json build/ model_create.ts public +p.ts \ No newline at end of file diff --git a/server/.vscode/settings.json b/server/.vscode/settings.json new file mode 100644 index 0000000..5d5ece7 --- /dev/null +++ b/server/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["fileupload", "Metadatas", "readir"] +} diff --git a/server/package.json b/server/package.json index a0cd0d3..d062c30 100644 --- a/server/package.json +++ b/server/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "pretest": "tsc", + "test:dev": "NODE_ENV=test_dev tsc-watch --onSuccess 'ts-node ./build/test/test.js'", "test:unit": "NODE_ENV=unit tsc-watch --onSuccess 'ts-node ./build/test/test.js'", "test:e2e": "NODE_ENV=e2e tsc-watch --onSuccess 'ts-node ./build/test/test.js'", "dev": "NODE_ENV=dev tsc-watch --onSuccess 'ts-node ./build/src/main.js'" diff --git a/server/src/core/controllers/app.ts b/server/src/core/controllers/app.ts index 05ca80b..00b8bc9 100644 --- a/server/src/core/controllers/app.ts +++ b/server/src/core/controllers/app.ts @@ -1,11 +1,11 @@ import express from "express"; -import { Routes } from "../interfaces/router"; import cors from "cors"; +import fileUpload from "express-fileupload"; +import { Routes } from "../interfaces/router"; import { Server } from "socket.io"; import { createServer } from "http"; import { SocketSubscriber } from "./socket_controller"; import { dirname } from "path"; -import fileUpload from "express-fileupload"; import { SetLastActivePipelineToRealTimeServiceScenario } from "../scenarios/set_active_pipeline_to_realtime_service_scenario"; import { CheckAndCreateStaticFilesFolderUseCase } from "../usecases/check_and_create_static_files_folder_usecase"; import { DataBaseConnectUseCase } from "../usecases/database_connect_usecase"; @@ -13,7 +13,7 @@ import { TypedEvent } from "../helpers/typed_event"; export enum ServerStatus { init = "init", - finished = "finshed", + finished = "finished", error = "error", } export enum Environment { @@ -85,7 +85,7 @@ export class App extends TypedEvent { this.app.use(cors()); this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); - this.app.use(express.static("public")); + this.app.use(express.static(App.staticFilesStoreDir())); this.app.use( fileUpload({ @@ -118,6 +118,7 @@ export class App extends TypedEvent { static staticFilesStoreDir = () => { const dir = dirname(__filename); const rootDir = dir.slice(0, dir.length - 20); + return rootDir + "public/"; }; } diff --git a/server/src/core/controllers/http_controller.ts b/server/src/core/controllers/http_controller.ts index ae3b30d..7d9d6b9 100644 --- a/server/src/core/controllers/http_controller.ts +++ b/server/src/core/controllers/http_controller.ts @@ -2,20 +2,20 @@ import { validationModelMiddleware } from "../middlewares/validation_model"; import { Result } from "../helpers/result"; import { Router, Request, Response } from "express"; import { IRouteModel, Routes } from "../interfaces/router"; +import { CoreValidation } from "../validations/core_validation"; export type HttpMethodType = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "PATCH" | "HEAD"; export type ResponseBase = Promise>; - export abstract class CallbackStrategyWithEmpty { abstract call(): ResponseBase; } export abstract class CallbackStrategyWithValidationModel { abstract validationModel: V; - abstract call(a: V): ResponseBase; + abstract call(model: V): ResponseBase; } export abstract class CallbackStrategyWithIdQuery { - abstract idValidationExpression: RegExp | null; + abstract idValidationExpression: CoreValidation; abstract call(id: string): ResponseBase; } export abstract class CallBackStrategyWithQueryPage { @@ -25,7 +25,8 @@ export abstract class CallBackStrategyWithQueryPage { export abstract class CallbackStrategyWithFileUpload { abstract checkingFileExpression: RegExp; - abstract call(file: File): ResponseBase; + abstract idValidationExpression: CoreValidation; + abstract call(file: File, id: string, description: string): ResponseBase; } interface ISubSetFeatureRouter { @@ -84,7 +85,24 @@ export class CoreHttpController implements ICoreHttpController { throw Error("needs to be implimed"); } if (el.fn instanceof CallbackStrategyWithIdQuery) { - throw Error("needs to be implimed"); + if (req.query.id === undefined) { + res.status(400).json("request query id is null, need query id ?id={id:String}"); + return; + } + if (el.fn.idValidationExpression !== undefined) { + if (!el.fn.idValidationExpression.regExp.test(req.query.id)) { + res + .status(400) + .json( + `request query id must fall under the pattern: ${el.fn.idValidationExpression.regExp} message: ${el.fn.idValidationExpression.message} ` + ); + return; + } else { + await this.responseHelper(res, el.fn.call(req.query.id)); + } + } else { + await this.responseHelper(res, el.fn.call(req["files"]["file"])); + } } if (el.fn instanceof CallBackStrategyWithQueryPage) { throw Error("needs to be implimed"); @@ -99,17 +117,32 @@ export class CoreHttpController implements ICoreHttpController { res.status(400).json("need files to form-data request"); return; } + if (req["files"]["file"] === undefined) { res.status(400).json("need file to form data request"); return; } + if (req.query.description === undefined) { + res + .status(400) + .json("request query description is null, need query description &description={description:String}"); + return; + } + if (req.query.id === undefined) { + res.status(400).json("request query id is null, need query id ?id={id:String}"); + return; + } + if (!el.fn.idValidationExpression.regExp.test(req.query.id)) { + res.status(400).json(el.fn.idValidationExpression.message); + return; + } if (el.fn instanceof CallbackStrategyWithFileUpload) { if (!el.fn.checkingFileExpression.test(req["files"]["file"]["name"])) { res.status(400).json("a file with this extension is expected: " + String(el.fn.checkingFileExpression)); return; } } - await this.responseHelper(res, el.fn.call(req["files"]["file"])); + await this.responseHelper(res, el.fn.call(req["files"]["file"], req.query.id, req.query.description)); } }); }); diff --git a/server/src/core/controllers/routes.ts b/server/src/core/controllers/routes.ts index 76e8f90..b76963d 100644 --- a/server/src/core/controllers/routes.ts +++ b/server/src/core/controllers/routes.ts @@ -1,7 +1,10 @@ import { NixStoreManagerPresentation } from "../../features/nix_store_manager/nix_store_manager"; import { PipelinePresentation } from "../../features/pipelines/pipeline_presentation"; import { ProcessPresentation } from "../../features/process/process_presentation"; -import { ProjectInstancePresentation } from "../../features/project_instance/project_instance_presentation"; +import { + ProjectInstancePresentation, + RobossemblerAssetsPresentation, +} from "../../features/project_instance/project_instance_presentation"; import { ProjectsPresentation } from "../../features/projects/projects_presentation"; import { RealTimePresentation } from "../../features/realtime/realtime_presentation"; import { TriggerPresentation } from "../../features/triggers/triggers_presentation"; @@ -21,6 +24,7 @@ export const httpRoutes: Routes[] = [ new RealTimePresentation(), new ProjectInstancePresentation(), new NixStoreManagerPresentation(), + new RobossemblerAssetsPresentation(), ] .concat(routersImplementPureCrud) .map((el) => el.call()); diff --git a/server/src/core/extensions/buffer.ts b/server/src/core/extensions/buffer.ts new file mode 100644 index 0000000..ee7442f --- /dev/null +++ b/server/src/core/extensions/buffer.ts @@ -0,0 +1,8 @@ +export const BufferExtensions = () => { + if (Buffer.joinBuffers === undefined) { + Buffer.prototype.joinBuffers = function (buffers: Array, delimiter = " ") { + const d = Buffer.from(delimiter); + return buffers.reduce((prev, b) => Buffer.concat([prev, d, b])); + }; + } +}; diff --git a/server/src/core/extensions/extensions.ts b/server/src/core/extensions/extensions.ts index 14e8907..b945e51 100644 --- a/server/src/core/extensions/extensions.ts +++ b/server/src/core/extensions/extensions.ts @@ -1,4 +1,5 @@ import { ArrayExtensions } from "./array"; +import { BufferExtensions } from "./buffer"; import { StringExtensions } from "./string"; declare global { @@ -10,15 +11,20 @@ declare global { isEmpty(): boolean; isNotEmpty(): boolean; } + interface BufferConstructor { + joinBuffers(buffers: Array, delimiter?: string); + } interface String { isEmpty(): boolean; isNotEmpty(): boolean; lastElement(): string; hasPattern(pattern: string): boolean; hasNoPattern(pattern: string): boolean; + pathNormalize(): string; } } export const extensions = () => { ArrayExtensions(); StringExtensions(); + BufferExtensions(); }; diff --git a/server/src/core/extensions/string.ts b/server/src/core/extensions/string.ts index bdd093d..055c615 100644 --- a/server/src/core/extensions/string.ts +++ b/server/src/core/extensions/string.ts @@ -5,6 +5,11 @@ export const StringExtensions = () => { return this.length === 0; }; } + if ("".pathNormalize === undefined) { + String.prototype.pathNormalize = function () { + return this.replace("//", "/"); + }; + } if ("".isNotEmpty === undefined) { // eslint-disable-next-line no-extend-native String.prototype.isNotEmpty = function () { @@ -27,3 +32,6 @@ export const StringExtensions = () => { }; } }; +// python3 /Users/idontsudo/framework/path.py --path /Users/idontsudo/webservice/server/build/public/0a3422cc-f2e3-4abc-87d8-ae13b8b6d26d/ --env /Users/idontsudo/framework/cad_generation/env.json +// python3 /Users/idontsudo/framework/path.py --path /Users/idontsudo/webservice/server/build/public/0a3422cc-f2e3-4abc-87d8-ae13b8b6d26d/ --env /Users/idontsudo/framework/cad_generation/env.json +// /Users/idontsudo/Desktop/FreeCAD.app/Contents/MacOS/FreeCAD /Users/idontsudo/framework/cad_generation/main.py diff --git a/server/src/core/helpers/typed_event.ts b/server/src/core/helpers/typed_event.ts index e7002c3..fe84f7b 100644 --- a/server/src/core/helpers/typed_event.ts +++ b/server/src/core/helpers/typed_event.ts @@ -8,8 +8,19 @@ export interface Disposable { export class TypedEvent { private listeners: Listener[] = []; + public listenersOnces: Listener[] = []; + waitedEvent(predicate: (e: T) => boolean) { + return new Promise((resolve, _reject) => { + this.on((e) => { + const isContinueWatching = predicate(e); + if (!isContinueWatching) { + resolve(e); + } + }); + }); + } on = (listener: Listener): Disposable => { this.listeners.push(listener); return { diff --git a/server/src/core/helpers/worker_computed.ts b/server/src/core/helpers/worker_computed.ts index a5ab80a..bc88fad 100644 --- a/server/src/core/helpers/worker_computed.ts +++ b/server/src/core/helpers/worker_computed.ts @@ -17,7 +17,9 @@ export interface WorkerDataExec { process.on("message", async (message) => { const workerData = message as WorkerDataExec; if (workerData.type == WorkerType.SPAWN) { - const subprocess = cp.spawn(workerData.command, workerData.cliArgs, { + // Maybe error + // const subprocess = cp.spawn(workerData.command, workerData.cliArgs, { + const subprocess = cp.spawn(workerData.command, { cwd: workerData.execPath, }); diff --git a/server/src/core/interfaces/payload.ts b/server/src/core/interfaces/payload.ts deleted file mode 100644 index d9f7e0a..0000000 --- a/server/src/core/interfaces/payload.ts +++ /dev/null @@ -1,13 +0,0 @@ -// export class Payload{ -// model: T | undefined -// query:string | undefined -// setModel(model:T){ -// this.model = model -// } -// setQuery(query:string){ -// this.query = query -// } -// isEmpty(){ -// return this.model != undefined || this.query != undefined -// } -// } diff --git a/server/src/core/middlewares/validation_auth.ts b/server/src/core/middlewares/validation_auth.ts index 40f88d5..bea5b04 100644 --- a/server/src/core/middlewares/validation_auth.ts +++ b/server/src/core/middlewares/validation_auth.ts @@ -1,12 +1,6 @@ import { RequestHandler } from "express"; -export const validationMiddleware = ( - type: any, - value = "body", - skipMissingProperties = false, - whitelist = true, - forbidNonWhitelisted = true -): RequestHandler => { +export const validationMiddleware = (): RequestHandler => { // TODO:(IDONTSUDO) need TOKEN // return nextTick return (req, res, next) => {}; diff --git a/server/src/core/middlewares/validation_model.ts b/server/src/core/middlewares/validation_model.ts index ec5986d..91525e2 100644 --- a/server/src/core/middlewares/validation_model.ts +++ b/server/src/core/middlewares/validation_model.ts @@ -6,7 +6,7 @@ export const validationModelMiddleware = ( type: any, value = "body", skipMissingProperties = false, - whitelist = true, + whitelist = false, forbidNonWhitelisted = true ): RequestHandler => { return (req, res, next) => { diff --git a/server/src/core/models/active_pipeline_model.ts b/server/src/core/models/active_pipeline_model.ts index 9fa4c4e..3c1d920 100644 --- a/server/src/core/models/active_pipeline_model.ts +++ b/server/src/core/models/active_pipeline_model.ts @@ -1,24 +1,27 @@ export class ActivePipeline { pipelineIsRunning: boolean; - projectUUID?: string | null; + projectId?: string | null; lastProcessCompleteCount: number | null; error: any; + rootDir: string; path: string; constructor( pipelineIsRunning: boolean, lastProcessCompleteCount: number | null, error: any, path: string | null, - projectUUID?: string | null + projectId: string | null, + rootDir: string | null ) { this.pipelineIsRunning = pipelineIsRunning; - this.projectUUID = projectUUID; + this.projectId = projectId; this.lastProcessCompleteCount = lastProcessCompleteCount; this.error = error; this.path = path; + this.rootDir = rootDir; } static empty() { - return new ActivePipeline(false, null, null, null, null); + return new ActivePipeline(false, null, null, null, null, null); } } diff --git a/server/src/core/models/exec_error_model.ts b/server/src/core/models/exec_error_model.ts index bbb9f38..12cb3ac 100644 --- a/server/src/core/models/exec_error_model.ts +++ b/server/src/core/models/exec_error_model.ts @@ -4,10 +4,15 @@ extensions(); export class ExecError extends Error { 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); + try { + if (e) { + if ("type" in e && "script" in e && "unixTime" in e && "error" in e) { + return new ExecError(e.type, e.event, e.data); + } + } + } catch (error) { + console.log(error); } - return; } script: string; unixTime: number; @@ -34,10 +39,15 @@ export class SpawnError extends Error { this.unixTime = Date.now(); } static isError(errorType: any): SpawnError | void { - if ("command" in errorType && "error" in errorType && "execPath" in errorType) { - return new SpawnError(errorType.command, errorType.execPath, errorType.error); + try { + if (errorType) { + if ("command" in errorType && "error" in errorType && "execPath" in errorType) { + return new SpawnError(errorType.command, errorType.execPath, errorType.error); + } + } + } catch (error) { + console.log(error); } - return; } } diff --git a/server/src/core/models/executor_result.ts b/server/src/core/models/executor_result.ts index 8df94ab..fd4c32a 100644 --- a/server/src/core/models/executor_result.ts +++ b/server/src/core/models/executor_result.ts @@ -10,9 +10,14 @@ export class ExecutorResult { this.data = data; } static isExecutorResult(value: any): void | ExecutorResult { - if ("type" in value && "event" in value && "data" in value) { - return new ExecutorResult(value.type, value.event, value.data); + try { + if (value) { + if ("type" in value && "event" in value && "data" in value) { + return new ExecutorResult(value.type, value.event, value.data); + } + } + } catch (error) { + console.log(error); } - return; } } diff --git a/server/src/core/models/robossembler_assets.ts b/server/src/core/models/robossembler_assets.ts new file mode 100644 index 0000000..d0a99f2 --- /dev/null +++ b/server/src/core/models/robossembler_assets.ts @@ -0,0 +1,160 @@ +import { IsArray, IsEnum, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +export class Gravity { + @IsNumber() + x: number; + @IsNumber() + y: number; + @IsNumber() + z: number; +} + +export class Pose { + @IsNumber() + x: number; + @IsNumber() + y: number; + @IsNumber() + z: number; + @IsNumber() + roll: number; + @IsNumber() + pitch: number; + @IsNumber() + yaw: number; +} + +export class Position { + @IsNumber() + x: number; + @IsNumber() + y: number; + @IsNumber() + z: number; +} + +export enum InstanceType { + RGB_CAMERA = "rgb_camera", + SCENE_SIMPLE_OBJECT = "scene_simple_object", +} + +abstract class CoreInstances {} + +export class Instance extends CoreInstances { + @IsEnum(InstanceType) + instanceType: InstanceType; + @Type(() => Position) + position: Position; + @IsArray() + quaternion: number[]; + @IsOptional() + @IsString() + instanceAt: null | string = null; +} + +export class SceneSimpleObject extends Instance {} + +export class InstanceRgbCamera extends Instance { + @IsString() + cameraLink: string; + @IsString() + topicCameraInfo: string; + @IsOptional() + @IsString() + topicDepth: string | null; + @IsString() + topicImage: string; +} +export class Asset { + @IsString() + name: string; + @IsString() + ixx: string; + @IsString() + ixy: string; + @IsString() + ixz: string; + @IsString() + iyy: string; + @IsString() + izz: string; + @IsString() + mass: string; + @IsString() + posX: string; + @IsString() + posY: string; + @IsString() + posZ: string; + @IsString() + eulerX: string; + @IsString() + eulerY: string; + @IsString() + eulerZ: string; + @IsString() + iyz: string; + @IsString() + meshPath: string; + @IsString() + friction: string; + @IsString() + centerMassX: string; + @IsString() + centerMassY: string; + @IsString() + centerMassZ: string; +} + +export class Physics { + @IsString() + engine_name: string; + @Type(() => Gravity) + gravity: Gravity; +} + +export class RobossemblerAssets { + @ValidateNested() + @Type(() => Asset) + assets: Asset[]; + + @IsArray() + @Type(() => Instance, { + discriminator: { + property: "type", + subTypes: [ + { value: InstanceRgbCamera, name: InstanceType.RGB_CAMERA }, + { value: SceneSimpleObject, name: InstanceType.SCENE_SIMPLE_OBJECT }, + ], + }, + keepDiscriminatorProperty: true, + }) + instances: Instance[]; + + @IsOptional() + @ValidateNested() + @Type(() => Physics) + physics: Physics; + + convertLocalPathsToServerPaths(server_address: string): RobossemblerAssets { + this.assets = this.assets.map((el) => { + el.meshPath = server_address + el.meshPath; + return el; + }); + return this; + } + + getAssetPath(assetName: string): string { + const findElement = this.assets.find((el) => el.name === assetName); + + if (findElement === undefined) { + throw new Error("RobossemblerAssets.getAssetPath not found asset by name:" + assetName); + } + return findElement.meshPath; + } + + getAssetAtInstance(instanceAt: string): Asset { + return this.assets.filter((el) => el.name === instanceAt)[0]; + } +} diff --git a/server/src/core/models/static_files.ts b/server/src/core/models/static_files.ts new file mode 100644 index 0000000..dfe36c4 --- /dev/null +++ b/server/src/core/models/static_files.ts @@ -0,0 +1,3 @@ +export enum StaticFiles { + robossembler_assets = "robossembler_assets.json", +} diff --git a/server/src/core/repository/file_system_repository.ts b/server/src/core/repository/file_system_repository.ts new file mode 100644 index 0000000..07d4422 --- /dev/null +++ b/server/src/core/repository/file_system_repository.ts @@ -0,0 +1,43 @@ +import * as fs from "fs"; +import { promisify } from "node:util"; + +export class FileSystemRepository { + public createDir = promisify(fs.mkdir); + public lsStat = promisify(fs.lstat); + public writeFileAsync = promisify(fs.writeFile); + public dirIsExists = promisify(fs.exists); + public stat = promisify(fs.stat); + public readFileAsync = promisify(fs.readFile); + public readdir = promisify(fs.readdir); + + async readFileAtBuffer(path: string): Promise { + if ((await this.lsStat(path)).isDirectory()) { + return ( + await this.readdir(path, { + encoding: "buffer", + }) + ).reduce((accumulator, currentValue) => Buffer.joinBuffers([accumulator, currentValue]), Buffer.from("")); + } + return await this.readFileAsync(path); + } + + readDirRecursive(path: string, filesToDir: string[] = []): string[] { + const files = fs.readdirSync(path); + files.forEach((file) => { + let filePath = ""; + if (path[path.length - 1] !== "/") { + filePath = `${path}/${file}`; + } else { + filePath = `${path}${file}`; + } + + const stats = fs.statSync(filePath); + if (stats.isDirectory()) { + this.readDirRecursive(filePath, filesToDir); + } else { + filesToDir.push(file); + } + }); + return filesToDir; + } +} diff --git a/server/src/core/repository/fs.ts b/server/src/core/repository/fs.ts deleted file mode 100644 index 138681c..0000000 --- a/server/src/core/repository/fs.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as fs from "fs"; -import { promisify } from "node:util"; - -export const readFileAsync = promisify(fs.readFile); -export const readdir = promisify(fs.readdir); -export const stat = promisify(fs.stat); -export const lsStat = promisify(fs.lstat); -export const createDir = promisify(fs.mkdir); -export const dirIsExists = promisify(fs.exists); - -export const writeFileAsync = promisify(fs.writeFile); - -export function readDirRecursive(path: string, filesToDir: string[] = []) { - const files = fs.readdirSync(path); - files.forEach((file) => { - let filePath = ""; - if (path[path.length - 1] !== "/") { - filePath = `${path}/${file}`; - } else { - filePath = `${path}${file}`; - } - - const stats = fs.statSync(filePath); - if (stats.isDirectory()) { - readDirRecursive(filePath, filesToDir); - } else { - filesToDir.push(file); - } - }); - return filesToDir; -} diff --git a/server/src/core/scenarios/read_file_and_json_to_plain_instance_class_scenario.ts b/server/src/core/scenarios/read_file_and_json_to_plain_instance_class_scenario.ts new file mode 100644 index 0000000..03738ba --- /dev/null +++ b/server/src/core/scenarios/read_file_and_json_to_plain_instance_class_scenario.ts @@ -0,0 +1,33 @@ +import { ClassConstructor, plainToInstance } from "class-transformer"; +import { ReadFileAndParseJsonUseCase } from "../usecases/read_file_and_parse_json"; +import { Result } from "../helpers/result"; +import { validate, ValidationError } from "class-validator"; +const skipMissingProperties = false, + whitelist = false, + forbidNonWhitelisted = true; + +export class ReadingJsonFileAndConvertingToInstanceClassScenario { + model: ClassConstructor; + constructor(cls: ClassConstructor) { + this.model = cls; + } + call = async (path: string): Promise> => { + try { + const result = await new ReadFileAndParseJsonUseCase().call(path); + if (result.isFailure()) { + return result.forward(); + } + const json = result.value; + const model = plainToInstance(this.model, json); + const errors = await validate(model as object, { skipMissingProperties, whitelist, forbidNonWhitelisted }); + if (errors.length > 0) { + const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(", "); + return Result.error("ReadingJsonFileAndConvertingToInstanceClassScenario:" + message); + } else { + return Result.ok(model as T); + } + } catch (error) { + return Result.error("ReadingJsonFileAndConvertingToInstanceClassScenario" + String(error)); + } + }; +} diff --git a/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts b/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts index 8fac80e..5a8398a 100644 --- a/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts +++ b/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts @@ -4,20 +4,29 @@ import { } from "../../features/project_instance/models/project_instance_database_model"; import { pipelineRealTimeService } from "../../features/realtime/realtime_presentation"; import { App } from "../controllers/app"; -import { CreateFolderUseCase } from "../usecases/crete_folder_usecase"; +import { CreateFolderUseCase } from "../usecases/create_folder_usecase"; import { SearchDataBaseModelUseCase } from "../usecases/search_database_model_usecase"; export class SetLastActivePipelineToRealTimeServiceScenario { call = async (): Promise => { - const result = await new SearchDataBaseModelUseCase(ProjectInstanceDbModel).call({ - isActive: true, - }); - - if (result.isSuccess()) { - const projectModel = result.value; - const projectPath = App.staticFilesStoreDir() + result.value.rootDir + "/"; - await new CreateFolderUseCase().call(projectPath); - pipelineRealTimeService.setPipelineDependency(projectModel.project.pipelines, projectPath, projectModel._id); - } + return ( + await new SearchDataBaseModelUseCase(ProjectInstanceDbModel).call({ + isActive: true, + }) + ).fold( + async (projectModel) => { + const projectPath = App.staticFilesStoreDir() + projectModel.rootDir + "/"; + await new CreateFolderUseCase().call(projectPath); + pipelineRealTimeService.setPipelineDependency( + projectModel.project.pipelines, + projectPath, + projectModel._id, + projectModel.rootDir + ); + }, + async (_e) => { + console.log("not found active pipeline"); + } + ); }; } diff --git a/server/src/core/services/executor_program_service.ts b/server/src/core/services/executor_program_service.ts index 493046a..f6ed534 100644 --- a/server/src/core/services/executor_program_service.ts +++ b/server/src/core/services/executor_program_service.ts @@ -21,53 +21,61 @@ export class ExecutorProgramService constructor(execPath: string, maxTime: number | null = null) { super(); - this.execPath = execPath; + this.execPath = execPath.pathNormalize(); this.maxTime = maxTime; } private async workerExecuted(command: string, workerType: WorkerType, args: Array | undefined = undefined) { - cluster.setupPrimary({ - exec: "/Users/idontsudo/Desktop/testdeck-mocha-seed/server/build/src/core/helpers/worker_computed.js", - }); + try { + cluster.setupPrimary({ + exec: __dirname + "/../helpers/worker_computed.js", + }); - const worker = cluster.fork(); + const worker = cluster.fork(); - await delay(300); + await delay(300); - this.worker = worker; + this.worker = worker; - const workerDataExec: WorkerDataExec = { - command: command, - execPath: this.execPath, - type: workerType, - cliArgs: args, - }; - worker.send(workerDataExec); - worker.on("message", (e) => { - const spawnError = SpawnError.isError(e); - if (spawnError instanceof SpawnError) { - this.emit(Result.error(spawnError)); - return; + const workerDataExec: WorkerDataExec = { + command: command, + execPath: this.execPath, + type: workerType, + cliArgs: args, + }; + worker.send(workerDataExec); + worker.on("message", (e) => { + const spawnError = SpawnError.isError(e); + + if (spawnError instanceof SpawnError) { + this.emit(Result.error(spawnError)); + return; + } + const execError = ExecError.isExecError(e); + + if (execError instanceof ExecError) { + this.emit(Result.error(execError)); + return; + } + + const executorResult = ExecutorResult.isExecutorResult(e); + if (executorResult instanceof ExecutorResult) { + this.emit(Result.ok(executorResult)); + return; + } + }); + if (this.maxTime != null) { + setTimeout(() => { + this.worker.kill(); + this.emit( + Result.error( + WorkerType.EXEC ? new ExecError(command, "timeout err") : new SpawnError(command, "timeout err") + ) + ); + }, this.maxTime!); } - const executorResult = ExecutorResult.isExecutorResult(e); - if (executorResult instanceof ExecutorResult) { - this.emit(Result.ok(executorResult)); - return; - } - - const execError = ExecError.isExecError(e); - if (execError instanceof ExecError) { - this.emit(Result.error(execError)); - return; - } - }); - if (this.maxTime != null) { - setTimeout(() => { - this.worker.kill(); - this.emit( - Result.error(WorkerType.EXEC ? new ExecError(command, "timeout err") : new SpawnError(command, "timeout err")) - ); - }, this.maxTime!); + } catch (error) { + console.log(error); } } @@ -78,6 +86,7 @@ export class ExecutorProgramService return; } this.workerExecuted(command, WorkerType.SPAWN, args); + return; } } diff --git a/server/src/core/services/files_change_notifier_service.ts b/server/src/core/services/files_change_notifier_service.ts index 76bea96..6d422c5 100644 --- a/server/src/core/services/files_change_notifier_service.ts +++ b/server/src/core/services/files_change_notifier_service.ts @@ -5,23 +5,7 @@ import { BinaryLike } from "crypto"; import { EventsFileChanger, MetaDataFileManagerModel } from "../models/meta_data_file_manager_model"; import { Result } from "../helpers/result"; import { TypedEvent } from "../helpers/typed_event"; -import { lsStat, readFileAsync, readdir, stat } from "../repository/fs"; - -function joinBuffers(buffers: Array, delimiter = " ") { - const d = Buffer.from(delimiter); - return buffers.reduce((prev, b) => Buffer.concat([prev, d, b])); -} - -async function readFileAtBuffer(path: string): Promise { - if ((await lsStat(path)).isDirectory()) { - return ( - await readdir(path, { - encoding: "buffer", - }) - ).reduce((accumulator, currentValue) => joinBuffers([accumulator, currentValue]), Buffer.from("")); - } - return await readFileAsync(path); -} +import { FileSystemRepository } from "../repository/file_system_repository"; function md5(content: Buffer | BinaryLike): Promise { return new Promise((resolve, _reject) => { @@ -41,12 +25,14 @@ export class FilesChangeNotifierService extends TypedEvent> implements IFilesChangeNotifierService { + fileSystemRepository: FileSystemRepository; watcher!: fs.FSWatcher; directory: string; constructor(directory: string) { super(); this.directory = directory; this.init(); + this.fileSystemRepository = new FileSystemRepository(); } hashes: IHashesCache = {}; async init() { @@ -56,18 +42,18 @@ export class FilesChangeNotifierService } } async setHash(file: string) { - const data = await readFileAsync(file); + const data = await this.fileSystemRepository.readFileAsync(file); const md5Current = await md5(data); this.hashes[file] = new MetaDataFileManagerModel(file, md5Current, EventsFileChanger.static); this.emit(Result.ok(this.hashes)); } async getFiles(dir: string): Promise { - const subdirs = await readdir(dir); + const subdirs = await new FileSystemRepository().readdir(dir); const files = await Promise.all( subdirs.map(async (subdir) => { const res = resolve(dir, subdir); - return (await stat(res)).isDirectory() ? this.getFiles(res) : res; + return (await this.fileSystemRepository.stat(res)).isDirectory() ? this.getFiles(res) : res; }) ); return files.reduce((a: string | any[], f: any) => a.concat(f), []); @@ -85,7 +71,7 @@ export class FilesChangeNotifierService fsWait = false; }, 100); try { - const file = await readFileAtBuffer(filePath); + const file = await this.fileSystemRepository.readFileAtBuffer(filePath); const md5Current = await md5(file); if (md5Current === md5Previous) { return; diff --git a/server/src/core/services/pipeline_real_time_service.ts b/server/src/core/services/pipeline_real_time_service.ts index 479b7b2..3213df4 100644 --- a/server/src/core/services/pipeline_real_time_service.ts +++ b/server/src/core/services/pipeline_real_time_service.ts @@ -3,7 +3,7 @@ import { ExecError } from "../models/exec_error_model"; import { ExecutorResult } from "../models/executor_result"; import { ActivePipeline } from "../models/active_pipeline_model"; import { IPipeline } from "../models/process_model"; -import { Iteration } from "./stack_service"; +import { Iteration, StackService } from "./stack_service"; export class PipelineRealTimeService extends TypedEvent { status: ActivePipeline; @@ -32,9 +32,8 @@ export class PipelineRealTimeService extends TypedEvent { this.iterationLogSaver(iteration); } - iterationLogSaver(iteration: Iteration): void { + iterationLogSaver(_iteration: Iteration): void { // throw new Error("Method not implemented."); - // citeration.result.data } iterationErrorObserver(iteration: Iteration): void { @@ -59,15 +58,17 @@ export class PipelineRealTimeService extends TypedEvent { this.status.pipelineIsRunning = false; } } - setPipelineDependency(pipelineModels: IPipeline[], path: string, projectUUID: string) { + setPipelineDependency(pipelineModels: IPipeline[], path: string, projectId: string, rootDir: string) { this.pipelineModels = pipelineModels; - this.status["projectUUID"] = projectUUID; + this.status["projectId"] = projectId; this.status["path"] = path; + this.status["rootDir"] = rootDir; } runPipeline(): void { - // const stack = new StackService(this.pipelineModels, this.path); - // this.status["pipelineIsRunning"] = true; - // stack.on(this.pipelineSubscriber); - // stack.call(); + const stack = new StackService(this.pipelineModels, this.status.path); + + 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 32d686e..336b758 100644 --- a/server/src/core/services/stack_service.ts +++ b/server/src/core/services/stack_service.ts @@ -11,7 +11,7 @@ import { Trigger } from "../../features/triggers/models/trigger_database_model"; export interface Iteration { hashes: IHashesCache | null; - process: IPipeline; + pipeline: IPipeline; result?: ExecError | SpawnError | ExecutorResult; } @@ -36,80 +36,61 @@ export class StackService extends TypedEvent implements IStackServi el = this.commandHandler(el); this.callStack.push({ hashes: null, - process: el, + pipeline: el, }); } } private commandHandler(processMetaData: IPipeline) { - processMetaData.process.command = processMetaData.process.command.replace("$PATH", this.path); + processMetaData.process.command = processMetaData.process.command.replace("$PATH", this.path).pathNormalize(); return processMetaData; } public async call() { - let inc = 0; - - for await (const el of this.callStack!) { - await this.execStack(inc, el); - inc += 1; + this.callStack.map(async (el, index) => { + await this.execStack(index, el); this.emit(this.callStack); - } + }); } async execStack(stackNumber: number, stackLayer: Iteration): Promise { const executorService = new ExecutorProgramService(this.path); - executorService.call(stackLayer.process.process.type, stackLayer.process.process.command); - + executorService.call(stackLayer.pipeline.process.type, stackLayer.pipeline.process.command); const filesChangeNotifierService = new FilesChangeNotifierService(this.path); filesChangeNotifierService.call(); - const result = await this.waitEvent>(executorService); - await delay(100); - if (result.isSuccess()) { - this.callStack[stackNumber].result = result.value; - this.callStack[stackNumber].hashes = filesChangeNotifierService.hashes; - const triggerResult = await this.triggerExec(stackLayer.process.trigger, stackNumber); - triggerResult.fold( - (s) => { - s; - }, - (e) => { - e; + const result = await executorService.waitedEvent((event: Result) => { + event.map((value) => { + if (value.event === EXEC_EVENT.END) { + return true; } - ); - } + }); + return false; + }); + + await delay(100); + result.map(async (el) => { + this.callStack.at(stackNumber).result = el; + this.callStack.at(stackNumber).hashes = filesChangeNotifierService.hashes; + (await this.triggerExec(stackLayer.pipeline.trigger, stackNumber)).map(() => { + filesChangeNotifierService.cancel(); + }); + }); - filesChangeNotifierService.cancel(); return; } - public waitEvent(stream: TypedEvent): Promise { - const promise = new Promise((resolve, reject) => { - const addListener = () => { - stream.on((e) => { - const event = e as Result; - event.fold( - (s) => { - if (s.event === EXEC_EVENT.END) { - resolve(e); - } - }, - (e) => { - reject(e); - } - ); - }); - }; - addListener(); - }); - return promise; - } - private async triggerExec(trigger: Trigger | null, stackNumber: number): Promise> { - if (trigger !== null) { - const hashes = this.callStack[stackNumber].hashes; - if (hashes != null) { - return await new TriggerService(trigger, hashes, this.path).call(); + private async triggerExec(trigger: Trigger | null, stackNumber: number): Promise> { + try { + if (trigger !== null) { + const hashes = this.callStack[stackNumber].hashes; + + if (hashes != null) { + await new TriggerService(trigger, hashes, this.path).call(); + } + throw new Error("Hashes is null"); } - throw new Error("Hashes is null"); + return Result.ok(); + } catch (error) { + return Result.error(error); } - return Result.ok(); } } diff --git a/server/src/core/usecases/check_and_create_static_files_folder_usecase.ts b/server/src/core/usecases/check_and_create_static_files_folder_usecase.ts index 5780d8b..17decc3 100644 --- a/server/src/core/usecases/check_and_create_static_files_folder_usecase.ts +++ b/server/src/core/usecases/check_and_create_static_files_folder_usecase.ts @@ -1,10 +1,15 @@ import { App } from "../controllers/app"; -import { dirIsExists } from "../repository/fs"; -import { CreateFolderUseCase } from "./crete_folder_usecase"; +import { FileSystemRepository } from "../repository/file_system_repository"; +import { CreateFolderUseCase } from "./create_folder_usecase"; export class CheckAndCreateStaticFilesFolderUseCase { + fileSystemRepository: FileSystemRepository; + + constructor() { + this.fileSystemRepository = new FileSystemRepository(); + } call = async (): Promise => { - if (await dirIsExists(App.staticFilesStoreDir())) { + if (await this.fileSystemRepository.dirIsExists(App.staticFilesStoreDir())) { return; } const createFolderUseCase = await new CreateFolderUseCase().call(App.staticFilesStoreDir()); diff --git a/server/src/core/usecases/create_file_usecase.ts b/server/src/core/usecases/create_file_usecase.ts index 5795336..e36b399 100644 --- a/server/src/core/usecases/create_file_usecase.ts +++ b/server/src/core/usecases/create_file_usecase.ts @@ -1,10 +1,15 @@ import { Result } from "../helpers/result"; -import { writeFileAsync } from "../repository/fs"; +import { FileSystemRepository } from "../repository/file_system_repository"; export class CreateFileUseCase { + fileSystemRepository: FileSystemRepository; + constructor() { + this.fileSystemRepository = new FileSystemRepository(); + } async call(path: string, buffer: Buffer): Promise> { try { - await writeFileAsync(path, buffer); + await this.fileSystemRepository.writeFileAsync(path.pathNormalize(), buffer); + return Result.ok(); } catch (err) { return Result.error(err as Error); diff --git a/server/src/core/usecases/create_folder_usecase.ts b/server/src/core/usecases/create_folder_usecase.ts new file mode 100644 index 0000000..5a007fe --- /dev/null +++ b/server/src/core/usecases/create_folder_usecase.ts @@ -0,0 +1,21 @@ +import { Result } from "../helpers/result"; +import { FileSystemRepository } from "../repository/file_system_repository"; + +export class CreateFolderUseCase { + fileSystemRepository: FileSystemRepository; + constructor() { + this.fileSystemRepository = new FileSystemRepository(); + } + call = async (path: string): Promise> => { + try { + if (await this.fileSystemRepository.dirIsExists(path)) { + return Result.ok("ok"); + } + await this.fileSystemRepository.createDir(path); + + return Result.ok("ok"); + } catch (error) { + return Result.error(error as Error); + } + }; +} diff --git a/server/src/core/usecases/crete_folder_usecase.ts b/server/src/core/usecases/crete_folder_usecase.ts deleted file mode 100644 index 58da331..0000000 --- a/server/src/core/usecases/crete_folder_usecase.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Result } from "../helpers/result"; -import { dirIsExists, createDir } from "../repository/fs"; - -export class CreateFolderUseCase { - call = async (path: string): Promise> => { - try { - if (await dirIsExists(path)) { - return Result.error(new Error("createFolderUseCase create dir ")); - } - await createDir(path); - - return Result.ok("ok"); - } catch (error) { - return Result.error(error as Error); - } - }; -} diff --git a/server/src/core/usecases/find_and_update_database_model_usecase.ts b/server/src/core/usecases/find_and_update_database_model_usecase.ts new file mode 100644 index 0000000..7f1e7d1 --- /dev/null +++ b/server/src/core/usecases/find_and_update_database_model_usecase.ts @@ -0,0 +1,11 @@ +export class FindPredicateModelAndUpdateDatabaseModelUseCase { + databaseModel: D; + + constructor(model) { + this.databaseModel = model; + } + async call(conditions: Partial, update: Partial) { + const dbModel = this.databaseModel as any; + dbModel.findOneAndUpdate(conditions, update); + } +} diff --git a/server/src/core/usecases/get_server_address_usecase.ts b/server/src/core/usecases/get_server_address_usecase.ts new file mode 100644 index 0000000..6a00cc6 --- /dev/null +++ b/server/src/core/usecases/get_server_address_usecase.ts @@ -0,0 +1,7 @@ +import { Result } from "../helpers/result"; + +export class GetServerAddressUseCase { + call = (): Result => { + return Result.ok("http://localhost:4001"); + }; +} diff --git a/server/src/core/usecases/read_file_and_parse_json.ts b/server/src/core/usecases/read_file_and_parse_json.ts new file mode 100644 index 0000000..4669621 --- /dev/null +++ b/server/src/core/usecases/read_file_and_parse_json.ts @@ -0,0 +1,25 @@ +import { Result } from "../helpers/result"; +import { FileSystemRepository } from "../repository/file_system_repository"; + +export class ReadFileAndParseJsonUseCase { + fileSystemRepository: FileSystemRepository; + + constructor() { + this.fileSystemRepository = new FileSystemRepository(); + } + async call(path: string): Promise> { + try { + if (RegExp(path).test("^(.+)/([^/]+)$")) { + return Result.error(`ReadFileAndParseJsonUseCase got the bad way: ${path}`); + } + const file = await this.fileSystemRepository.readFileAsync(path); + try { + return Result.ok(JSON.parse(file.toString())); + } catch { + return Result.error(`ReadFileAndParseJsonUseCase is not json type file parse path: ${path}`); + } + } catch (error) { + return Result.error(`ReadFileAndParseJsonUseCase error:${error}`); + } + } +} diff --git a/server/src/core/usecases/write_file_system_file_usecase.ts b/server/src/core/usecases/write_file_system_file_usecase.ts new file mode 100644 index 0000000..57d9734 --- /dev/null +++ b/server/src/core/usecases/write_file_system_file_usecase.ts @@ -0,0 +1,13 @@ +import { Result } from "../helpers/result"; +import { FileSystemRepository } from "../repository/file_system_repository"; + +export class WriteFileSystemFileUseCase { + call = async (path: string, fileData: string): Promise> => { + try { + await new FileSystemRepository().writeFileAsync(path, fileData); + return Result.ok(undefined); + } catch (error) { + return Result.error(`WriteFileSystemFileUseCase error: ${error}`); + } + }; +} diff --git a/server/src/core/validations/core_validation.ts b/server/src/core/validations/core_validation.ts new file mode 100644 index 0000000..e6ef0f8 --- /dev/null +++ b/server/src/core/validations/core_validation.ts @@ -0,0 +1,8 @@ +interface IMessage { + message: string; +} + +export abstract class CoreValidation { + abstract regExp: RegExp; + abstract message: IMessage; +} diff --git a/server/src/core/validations/mongo_id_validation.ts b/server/src/core/validations/mongo_id_validation.ts new file mode 100644 index 0000000..68e9d3a --- /dev/null +++ b/server/src/core/validations/mongo_id_validation.ts @@ -0,0 +1,6 @@ +import { CoreValidation } from "./core_validation"; + +export class MongoIdValidation extends CoreValidation { + regExp = RegExp("^[0-9a-fA-F]{24}$"); + message = { message: "is do not mongo db object uuid" }; +} diff --git a/server/src/features/pipelines/pipeline_model.ts b/server/src/features/pipelines/pipeline_model.ts deleted file mode 100644 index 6491ed3..0000000 --- a/server/src/features/pipelines/pipeline_model.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { IsMongoId, IsEnum } from "class-validator"; -import { Schema, model } from "mongoose"; -import { TriggerModel, triggerSchema } from "../triggers/trigger_model"; -import { schemaProcess } from "../process/process_model"; -import { StackGenerateType } from "../../core/models/process_model"; - -export const PipelineSchema = new Schema({ - process: { - type: Schema.Types.ObjectId, - ref: schemaProcess, - autopopulate: true, - default: null, - }, - trigger: { - type: Schema.Types.ObjectId, - ref: triggerSchema, - autopopulate: true, - default: null, - }, - command: { - type: String, - }, -}).plugin(require("mongoose-autopopulate")); - -export const schemaPipeline = "Pipeline"; - -export const PipelineDBModel = model(schemaPipeline, PipelineSchema); - -export class PipelineModel { - @IsMongoId() - public process: PipelineModel; - - @IsMongoId() - //TODO(IDONTSUDO):NEED OPTION DECORATOR?? - public trigger: TriggerModel; - - public env = null; - - @IsEnum(StackGenerateType) - public stackGenerateType: StackGenerateType; -} diff --git a/server/src/features/process/process_model.ts b/server/src/features/process/process_model.ts deleted file mode 100644 index a8105c1..0000000 --- a/server/src/features/process/process_model.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { IsString, IsOptional, IsEnum, IsNumber, IsBoolean } from "class-validator"; -import { Schema, model } from "mongoose"; -import { IProcess, IssueType } from "../../core/models/process_model"; -import { EXEC_TYPE } from "../../core/models/exec_error_model"; - -export const ProcessSchema = new Schema({ - type: { - type: String, - }, - command: { - type: String, - }, - isGenerating: { - type: String, - }, - isLocaleCode: { - type: String, - }, - issueType: { - type: String, - }, - timeout: { - type: Number, - default: null, - }, - commit: { - type: String, - default: null, - }, -}); - -export const schemaProcess = "Process"; - -export const ProcessDBModel = model(schemaProcess, ProcessSchema); - -export class ProcessModel implements IProcess { - @IsEnum(EXEC_TYPE) - public type: EXEC_TYPE; - - @IsString() - public command: string; - - @IsBoolean() - public isGenerating: boolean; - - @IsBoolean() - public isLocaleCode: boolean; - - @IsEnum(IssueType) - public issueType: IssueType; - - @IsOptional() - @IsNumber() - public timeout?: number; - - @IsOptional() - @IsString() - public commit?: string; -} diff --git a/server/src/features/project_instance/domain/create_new_project_scenario.ts b/server/src/features/project_instance/domain/create_new_project_scenario.ts index b5a162a..76023b1 100644 --- a/server/src/features/project_instance/domain/create_new_project_scenario.ts +++ b/server/src/features/project_instance/domain/create_new_project_scenario.ts @@ -1,29 +1,21 @@ import { App } from "../../../core/controllers/app"; import { Result } from "../../../core/helpers/result"; import { CreateDataBaseModelUseCase } from "../../../core/usecases/create_database_model_usecase"; -import { CreateFolderUseCase } from "../../../core/usecases/crete_folder_usecase"; +import { CreateFolderUseCase } from "../../../core/usecases/create_folder_usecase"; +import { pipelineRealTimeService } from "../../realtime/realtime_presentation"; import { ProjectInstanceDbModel } from "../models/project_instance_database_model"; import { ProjectInstanceValidationModel } from "../models/project_instance_validation_model"; import { v4 as uuidv4 } from "uuid"; +import { SetActiveProjectScenario } from "./set_active_project_use_scenario"; export class CreateNewProjectInstanceScenario { - call = async (model: ProjectInstanceValidationModel): Promise> => { + call = async (): Promise> => { try { - const folderName = uuidv4() + "/"; - const createFolderUseCase = await new CreateFolderUseCase().call(App.staticFilesStoreDir() + folderName); - if (createFolderUseCase.isFailure()) { - return createFolderUseCase.forward(); - } - model.rootDir = folderName; - - const createDataBaseModelUseCase = await new CreateDataBaseModelUseCase(ProjectInstanceDbModel).call(model); - - if (createDataBaseModelUseCase.isFailure()) { - return createDataBaseModelUseCase.forward(); - } - - return Result.ok({ status: "ok" }); + // (await new SetActiveProjectScenario().call(id)).map(() => { + // return Result.ok({ status: "ok" }); + // }); } catch (error) { + console.log(error); return Result.error(error as Error); } }; diff --git a/server/src/features/project_instance/domain/robossembler_assets_network_mapper_scenario.ts b/server/src/features/project_instance/domain/robossembler_assets_network_mapper_scenario.ts new file mode 100644 index 0000000..ead9008 --- /dev/null +++ b/server/src/features/project_instance/domain/robossembler_assets_network_mapper_scenario.ts @@ -0,0 +1,33 @@ +import { CallbackStrategyWithEmpty, ResponseBase } from "../../../core/controllers/http_controller"; +import { Result } from "../../../core/helpers/result"; +import { RobossemblerAssets } from "../../../core/models/robossembler_assets"; +import { StaticFiles } from "../../../core/models/static_files"; +import { ReadingJsonFileAndConvertingToInstanceClassScenario } from "../../../core/scenarios/read_file_and_json_to_plain_instance_class_scenario"; +import { GetServerAddressUseCase } from "../../../core/usecases/get_server_address_usecase"; +import { PipelineStatusUseCase } from "../../realtime/domain/pipeline_status_usecase"; + +export class RobossemblerAssetsNetworkMapperScenario extends CallbackStrategyWithEmpty { + async call(): ResponseBase { + try { + const result = await new PipelineStatusUseCase().call(); + + return await result.map(async (activeInstanceModel) => { + return ( + await new ReadingJsonFileAndConvertingToInstanceClassScenario(RobossemblerAssets).call( + `${activeInstanceModel.path}${StaticFiles.robossembler_assets}` + ) + ).map((robossemblerAssets) => { + return new GetServerAddressUseCase().call().map((address) => { + return Result.ok( + robossemblerAssets.convertLocalPathsToServerPaths( + `${address}/${activeInstanceModel.rootDir.pathNormalize()}` + ) + ); + }); + }); + }); + } catch (error) { + return Result.error(error); + } + } +} diff --git a/server/src/features/project_instance/domain/save_active_scene_scenario.ts b/server/src/features/project_instance/domain/save_active_scene_scenario.ts new file mode 100644 index 0000000..6ab753a --- /dev/null +++ b/server/src/features/project_instance/domain/save_active_scene_scenario.ts @@ -0,0 +1,28 @@ +import { CallbackStrategyWithValidationModel, ResponseBase } from "../../../core/controllers/http_controller"; +import { Result } from "../../../core/helpers/result"; +import { RobossemblerAssets } from "../../../core/models/robossembler_assets"; +import { StaticFiles } from "../../../core/models/static_files"; +import { ReadingJsonFileAndConvertingToInstanceClassScenario } from "../../../core/scenarios/read_file_and_json_to_plain_instance_class_scenario"; +import { WriteFileSystemFileUseCase } from "../../../core/usecases/write_file_system_file_usecase"; +import { PipelineStatusUseCase } from "../../realtime/domain/pipeline_status_usecase"; + +export class SaveActiveSceneScenario extends CallbackStrategyWithValidationModel { + validationModel: RobossemblerAssets = new RobossemblerAssets(); + async call(model: RobossemblerAssets): ResponseBase { + return (await new PipelineStatusUseCase().call()).map(async (activeInstanceModel) => + ( + await new ReadingJsonFileAndConvertingToInstanceClassScenario(RobossemblerAssets).call( + `${activeInstanceModel.path}${StaticFiles.robossembler_assets}`.pathNormalize() + ) + ).map(async (prevModel) => { + model.assets = prevModel.assets; + return ( + await new WriteFileSystemFileUseCase().call( + `${activeInstanceModel.path}${StaticFiles.robossembler_assets}`.pathNormalize(), + JSON.stringify(model) + ) + ).map(() => Result.ok("assets is rewrite")); + }) + ); + } +} diff --git a/server/src/features/project_instance/domain/set_active_project_use_scenario.ts b/server/src/features/project_instance/domain/set_active_project_use_scenario.ts new file mode 100644 index 0000000..2d18dc8 --- /dev/null +++ b/server/src/features/project_instance/domain/set_active_project_use_scenario.ts @@ -0,0 +1,35 @@ +import { App } from "../../../core/controllers/app"; +import { CallbackStrategyWithIdQuery, ResponseBase } from "../../../core/controllers/http_controller"; +import { Result } from "../../../core/helpers/result"; +import { SetLastActivePipelineToRealTimeServiceScenario } from "../../../core/scenarios/set_active_pipeline_to_realtime_service_scenario"; +import { CreateFolderUseCase } from "../../../core/usecases/create_folder_usecase"; +import { ReadByIdDataBaseModelUseCase } from "../../../core/usecases/read_by_id_database_model_usecase"; +import { UpdateDataBaseModelUseCase } from "../../../core/usecases/update_database_model_usecase"; +import { MongoIdValidation } from "../../../core/validations/mongo_id_validation"; +import { IProjectInstanceModel, ProjectInstanceDbModel } from "../models/project_instance_database_model"; + +export class SetActiveProjectScenario extends CallbackStrategyWithIdQuery { + idValidationExpression = new MongoIdValidation(); + + async call(id: string): ResponseBase { + const result = await new ReadByIdDataBaseModelUseCase(ProjectInstanceDbModel).call(id); + // id + + if (result.isFailure()) { + return result.forward(); + } + const model = result.value; + + return await ( + await new CreateFolderUseCase().call(App.staticFilesStoreDir() + model.rootDir) + ).map(async () => { + model.isActive = true; + return (await new UpdateDataBaseModelUseCase(ProjectInstanceDbModel).call(model)).map(async (el) => { + // TODO(IDONTSUDO): move it to a separate UseCase + await ProjectInstanceDbModel.updateMany({ _id: { $ne: el._id }, isActive: { $eq: true } }, { isActive: false }); + await new SetLastActivePipelineToRealTimeServiceScenario().call(); + return Result.ok(`project ${id} is active`); + }); + }); + } +} diff --git a/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts b/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts index e2ab3e5..deb06e8 100644 --- a/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts +++ b/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts @@ -1,23 +1,37 @@ +import { App } from "../../../core/controllers/app"; import { CallbackStrategyWithFileUpload, ResponseBase } from "../../../core/controllers/http_controller"; import { Result } from "../../../core/helpers/result"; import { IFile } from "../../../core/interfaces/file"; +import { CreateDataBaseModelUseCase } from "../../../core/usecases/create_database_model_usecase"; import { CreateFileUseCase } from "../../../core/usecases/create_file_usecase"; -import { PipelineStatusUseCase } from "../../realtime/domain/pipeline_status_usecase"; +import { CreateFolderUseCase } from "../../../core/usecases/create_folder_usecase"; +import { MongoIdValidation } from "../../../core/validations/mongo_id_validation"; +import { ProjectInstanceDbModel } from "../models/project_instance_database_model"; +import { ProjectInstanceValidationModel } from "../models/project_instance_validation_model"; +import { v4 as uuidv4 } from "uuid"; +import { SetActiveProjectScenario } from "./set_active_project_use_scenario"; export class UploadCadFileToProjectScenario extends CallbackStrategyWithFileUpload { checkingFileExpression: RegExp = RegExp(".FCStd"); + idValidationExpression = new MongoIdValidation(); - async call(file: IFile): ResponseBase { - const pipelineStatusUseCase = await new PipelineStatusUseCase().call(); - if (pipelineStatusUseCase.isFailure()) { - return pipelineStatusUseCase.forward(); - } - const projectFolder = pipelineStatusUseCase.value.path; - const createFileUseCase = await new CreateFileUseCase().call(projectFolder + file.name, file.data); - if (createFileUseCase.isFailure()) { - return createFileUseCase.forward(); - } - - return Result.ok("ok"); + async call(file: IFile, id: string, description: string): ResponseBase { + const folderName = uuidv4() + "/"; + const model = new ProjectInstanceValidationModel(); + model["project"] = id; + model["description"] = description; + model["rootDir"] = folderName; + model["isActive"] = true; + return (await new CreateFolderUseCase().call(App.staticFilesStoreDir() + folderName)).map(async () => + (await new CreateDataBaseModelUseCase(ProjectInstanceDbModel).call(model)).map(async (databaseModel) => + (await new SetActiveProjectScenario().call(databaseModel.id)).map(async () => + (await new CreateFileUseCase().call(App.staticFilesStoreDir() + folderName + file.name, file.data)).map( + () => { + return Result.ok("ok"); + } + ) + ) + ) + ); } } diff --git a/server/src/features/project_instance/models/project_instance_validation_model.ts b/server/src/features/project_instance/models/project_instance_validation_model.ts index 709cc94..cb3149a 100644 --- a/server/src/features/project_instance/models/project_instance_validation_model.ts +++ b/server/src/features/project_instance/models/project_instance_validation_model.ts @@ -9,4 +9,7 @@ export class ProjectInstanceValidationModel { @IsOptional() public rootDir: string; + + @IsOptional() + public isActive: boolean; } diff --git a/server/src/features/project_instance/project_instance_presentation.ts b/server/src/features/project_instance/project_instance_presentation.ts index 77040a8..5732113 100644 --- a/server/src/features/project_instance/project_instance_presentation.ts +++ b/server/src/features/project_instance/project_instance_presentation.ts @@ -1,5 +1,11 @@ import { CrudController } from "../../core/controllers/crud_controller"; +import { CoreHttpController } from "../../core/controllers/http_controller"; +import { RobossemblerAssets } from "../../core/models/robossembler_assets"; + import { CreateNewProjectInstanceScenario } from "./domain/create_new_project_scenario"; +import { RobossemblerAssetsNetworkMapperScenario } from "./domain/robossembler_assets_network_mapper_scenario"; +import { SaveActiveSceneScenario } from "./domain/save_active_scene_scenario"; +import { SetActiveProjectScenario } from "./domain/set_active_project_use_scenario"; import { UploadCadFileToProjectScenario } from "./domain/upload_file_to_to_project_scenario"; import { ProjectInstanceDbModel } from "./models/project_instance_database_model"; import { ProjectInstanceValidationModel } from "./models/project_instance_validation_model"; @@ -14,14 +20,30 @@ export class ProjectInstancePresentation extends CrudController< url: "project_instance", databaseModel: ProjectInstanceDbModel, }); + super.post(new CreateNewProjectInstanceScenario().call); - super.subRoutes = [ - { - method: "POST", - subUrl: "upload", - fn: new UploadCadFileToProjectScenario(), - }, - ]; + this.subRoutes.push({ + method: "POST", + subUrl: "set/active/project", + fn: new SetActiveProjectScenario(), + }); + + this.subRoutes.push({ + method: "POST", + subUrl: "upload", + fn: new UploadCadFileToProjectScenario(), + }); + } +} + +export class RobossemblerAssetsPresentation extends CoreHttpController { + constructor() { + super({ + url: "robossembler_assets", + validationModel: RobossemblerAssets, + }); + super.get(new RobossemblerAssetsNetworkMapperScenario().call); + super.post(new SaveActiveSceneScenario().call); } } diff --git a/server/src/features/projects/projects_model.ts b/server/src/features/projects/projects_model.ts deleted file mode 100644 index dfbf035..0000000 --- a/server/src/features/projects/projects_model.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Schema, model } from "mongoose"; -import { PipelineModel, schemaPipeline } from "../pipelines/pipeline_model"; -import { IsMongoId, IsString } from "class-validator"; - -export interface IProjectModel { - pipelines: [PipelineModel]; - rootDir: string; -} - -export const ProjectSchema = new Schema({ - pipelines: { - type: Array, - ref: schemaPipeline, - autopopulate: true, - default: null, - }, - rootDir: { - type: String, - }, -}).plugin(require("mongoose-autopopulate")); - -const schema = "Projects"; - -export const ProjectDBModel = model(schema, ProjectSchema); - -export class ProjectModel implements IProjectModel { - @IsMongoId() - pipelines: [PipelineModel]; - @IsString() - rootDir: string; -} diff --git a/server/src/features/realtime/domain/pipeline_status_usecase.ts b/server/src/features/realtime/domain/pipeline_status_usecase.ts index 96422c2..df47fe1 100644 --- a/server/src/features/realtime/domain/pipeline_status_usecase.ts +++ b/server/src/features/realtime/domain/pipeline_status_usecase.ts @@ -6,10 +6,11 @@ export class PipelineStatusUseCase { async call(): Promise> { try { const status = pipelineRealTimeService.status; - if (status.projectUUID !== null) { + if (status.projectId !== null) { return Result.ok(status); } - if (status.projectUUID === null) { + + if (status.projectId === null) { return Result.error(new Error("pipelineRealTimeService does not have an active project instance")); } } catch (error) { diff --git a/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts b/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts index 7fff00e..37c1c1a 100644 --- a/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts +++ b/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts @@ -1,41 +1,60 @@ import { App } from "../../../core/controllers/app"; +import { CallbackStrategyWithEmpty } from "../../../core/controllers/http_controller"; import { Result } from "../../../core/helpers/result"; +import { IPipeline } from "../../../core/models/process_model"; import { ReadByIdDataBaseModelUseCase } from "../../../core/usecases/read_by_id_database_model_usecase"; -import { UpdateDataBaseModelUseCase } from "../../../core/usecases/update_database_model_usecase"; +import { PipelineValidationModel } from "../../pipelines/models/pipeline_validation_model"; import { IProjectInstanceModel, ProjectInstanceDbModel, } from "../../project_instance/models/project_instance_database_model"; -import { RealTimeValidationModel, pipelineRealTimeService } from "../realtime_presentation"; +import { pipelineRealTimeService } from "../realtime_presentation"; +import { PipelineStatusUseCase } from "./pipeline_status_usecase"; -export class RunInstancePipelineUseCase { - async call(model: RealTimeValidationModel): Promise> { - const { id } = model; - const readByIdDataBaseModelUseCase = await new ReadByIdDataBaseModelUseCase( - ProjectInstanceDbModel - ).call(id); +const mongoPipelineModelMapper = (el: PipelineValidationModel): IPipeline => { + const mapObj: IPipeline = { + process: { + type: el.process.type, + command: el.process.command, + isGenerating: Boolean(el.process.isGenerating), + isLocaleCode: Boolean(el.process.isLocaleCode), + issueType: el.process.issueType, + }, + trigger: { + type: el.trigger.type, + value: el.trigger.value.map((el) => String(el)), + }, + env: null, + stackGenerateType: el.stackGenerateType, + }; + return mapObj; +}; - if (readByIdDataBaseModelUseCase.isFailure()) { - return readByIdDataBaseModelUseCase.forward(); - } +export class RunInstancePipelineUseCase extends CallbackStrategyWithEmpty { + async call(): Promise> { + return (await new PipelineStatusUseCase().call()).map(async (activePipelineModel) => { + if (activePipelineModel.pipelineIsRunning) { + return Result.error("pipeline is running"); + } + const readByIdDataBaseModelUseCase = await new ReadByIdDataBaseModelUseCase( + ProjectInstanceDbModel + ).call(activePipelineModel.projectId); - const projectModel = readByIdDataBaseModelUseCase.value; - projectModel.isActive = true; + if (readByIdDataBaseModelUseCase.isFailure()) { + return readByIdDataBaseModelUseCase.forward(); + } + const projectModel = readByIdDataBaseModelUseCase.value; + const resultMapper = projectModel.project.pipelines.map((el) => mongoPipelineModelMapper(el)); - const updateDataBaseModelUseCase = await new UpdateDataBaseModelUseCase( - ProjectInstanceDbModel - ).call(projectModel); + pipelineRealTimeService.setPipelineDependency( + resultMapper, + App.staticFilesStoreDir() + projectModel.rootDir + "/", + projectModel._id, + projectModel.rootDir + ); - if (updateDataBaseModelUseCase.isFailure()) { - return updateDataBaseModelUseCase.forward(); - } - pipelineRealTimeService.setPipelineDependency( - projectModel.project.pipelines, - App.staticFilesStoreDir() + projectModel.rootDir + "/", - projectModel._id - ); - pipelineRealTimeService.runPipeline(); - - return Result.ok({ status: "ok" }); + pipelineRealTimeService.runPipeline(); + return Result.ok("ok"); + }); } } diff --git a/server/src/features/realtime/realtime_presentation.ts b/server/src/features/realtime/realtime_presentation.ts index 406ec5d..249fca1 100644 --- a/server/src/features/realtime/realtime_presentation.ts +++ b/server/src/features/realtime/realtime_presentation.ts @@ -18,7 +18,11 @@ export class RealTimePresentation extends CoreHttpController(triggerSchema, TriggerSchema); - -export enum TriggerType { - PROCESS = "PROCESS", - FILE = "FILE", -} - -export class TriggerModel implements ITriggerModel { - @IsOptional() - public _id: string; - @IsEnum(TriggerType) - public type: TriggerType; - @IsArray() - public value: string[]; -} - -export interface Trigger { - type: TriggerType; - value: string[]; -} - \ No newline at end of file diff --git a/server/src/features/triggers/triggers_presentation.ts b/server/src/features/triggers/triggers_presentation.ts index f48f7dd..0300f15 100644 --- a/server/src/features/triggers/triggers_presentation.ts +++ b/server/src/features/triggers/triggers_presentation.ts @@ -1,13 +1,14 @@ import { CrudController } from "../../core/controllers/crud_controller"; import { TriggerDBModel } from "./models/trigger_database_model"; -import { TriggerModelValidationModel as TriggerValidationMode } from "./models/trigger_validation_model"; +import { TriggerModelValidationModel } from "./models/trigger_validation_model"; -export class TriggerPresentation extends CrudController { +export class TriggerPresentation extends CrudController { constructor() { super({ url: "trigger", - validationModel: TriggerValidationMode, + validationModel: TriggerModelValidationModel, databaseModel: TriggerDBModel, }); } } +"".isEmpty(); diff --git a/server/src/main.ts b/server/src/main.ts index 3b91a44..f2682ca 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -4,9 +4,11 @@ import { SocketSubscriber } from "./core/controllers/socket_controller"; import { extensions } from "./core/extensions/extensions"; import { httpRoutes } from "./core/controllers/routes"; import { pipelineRealTimeService } from "./features/realtime/realtime_presentation"; +import { main } from "./p"; extensions(); const socketSubscribers = [new SocketSubscriber(pipelineRealTimeService, "realtime")]; new App(httpRoutes, socketSubscribers).listen(); +main(); diff --git a/server/test/services/stack_service_test.ts b/server/test/services/stack_service_test.ts index 80d0bcc..9a47f15 100644 --- a/server/test/services/stack_service_test.ts +++ b/server/test/services/stack_service_test.ts @@ -3,19 +3,23 @@ import { StackService } from "../../src/core/services/stack_service"; import { delay } from "../../src/core/helpers/delay"; import { assert, dirname__ } from "../test"; import { mockSimplePipeline } from "../model/mock_pipelines"; -import { readDirRecursive } from "../../src/core/repository/fs"; +import { FileSystemRepository } from "../../src/core/repository/file_system_repository"; +import { CreateFolderUseCase } from "../../src/core/usecases/create_folder_usecase"; abstract class IStackServiceTest { abstract test(): Promise; } class SimpleTestStackServiceTest extends StackService implements IStackServiceTest { + fileSystemRepository: FileSystemRepository; constructor() { super(mockSimplePipeline, dirname__ + "/context/"); + this.fileSystemRepository = new FileSystemRepository(); } async test(): Promise { await this.call(); - const testResult = readDirRecursive(this.path).equals(["1.txt", "test.txt"], true); + console.log(this.path); + const testResult = this.fileSystemRepository.readDirRecursive(this.path).equals(["1.txt", "test.txt"], true); await delay(100); rmSync(this.path + "example/", { recursive: true }); return testResult; @@ -24,12 +28,15 @@ class SimpleTestStackServiceTest extends StackService implements IStackServiceTe export class StackServiceTest { dirName: string; - + fileSystemRepository: FileSystemRepository; constructor(dirName: string) { this.dirName = dirName; + this.fileSystemRepository = new FileSystemRepository(); } public async test() { const tests = [new SimpleTestStackServiceTest()]; + await new CreateFolderUseCase().call(this.dirName + "/context/"); + for await (const el of tests) { assert((await el.test()) === true, el.constructor.name); await delay(3000); diff --git a/server/test/test.ts b/server/test/test.ts index 1bbe197..d0487c5 100644 --- a/server/test/test.ts +++ b/server/test/test.ts @@ -39,14 +39,14 @@ const unitTest = async () => { await init(); await new ExecutorProgramServiceTest(dirname__).test(); await new FilesChangerTest(dirname__).test(); - await new StackServiceTest(dirname__ + "/context/").test(); + await new StackServiceTest(dirname__).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(); - // await new PipelineRealTimeServiceTest().test() + for await (const usecase of tests) { testCore.assert(await new usecase().test(), usecase.name); } @@ -54,7 +54,6 @@ const unitTest = async () => { const presentationCrudControllers = [new TriggerPresentation()]; const e2eTest = async () => { const app = new App(httpRoutes, [], Environment.E2E_TEST); - app.listen(); await new Promise((resolve, reject) => { app.on(async (e) => { if (e === ServerStatus.finished) { diff --git a/ui/package.json b/ui/package.json index f03a316..630e95c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,6 +13,8 @@ "@types/react-dom": "^18.2.7", "@types/socket.io-client": "^3.0.0", "@types/uuid": "^9.0.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "formik-antd": "^2.0.4", "i18next": "^23.6.0", "mobx": "^6.10.0", @@ -24,9 +26,13 @@ "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^6.18.0", "react-scripts": "5.0.1", + "reflect-metadata": "^0.1.13", "sass": "^1.66.1", "socket.io-client": "^4.7.2", "three": "^0.159.0", + "three-stdlib": "^2.28.9", + "three-transform-controls": "^1.0.4", + "ts-pattern": "^5.0.6", "typescript": "^4.9.5", "urdf-loader": "^0.12.1", "uuid": "^9.0.1", diff --git a/ui/src/core/extensions/extensions.ts b/ui/src/core/extensions/extensions.ts index 5723394..57a9789 100644 --- a/ui/src/core/extensions/extensions.ts +++ b/ui/src/core/extensions/extensions.ts @@ -2,7 +2,8 @@ import { ArrayExtensions } from "./array"; import { MapExtensions } from "./map"; import { StringExtensions } from "./string"; -export type CallBackFunction = (value: T) => void; +export type CallBackVoidFunction = (value: T) => void; +export type CallBackStringVoidFunction = (value: string) => void; declare global { interface Array { @@ -15,9 +16,10 @@ declare global { } interface String { isEmpty(): boolean; + isNotEmpty(): boolean; } interface Map { - addValueOrMakeCallback(key: K, value: V, callBack: CallBackFunction): void; + addValueOrMakeCallback(key: K, value: V, callBack: CallBackVoidFunction): void; } } export const extensions = () => { diff --git a/ui/src/core/extensions/string.ts b/ui/src/core/extensions/string.ts index 030607e..86b5246 100644 --- a/ui/src/core/extensions/string.ts +++ b/ui/src/core/extensions/string.ts @@ -5,4 +5,10 @@ export const StringExtensions = () => { return this.length === 0; }; } + if ("".isNotEmpty === undefined) { + // eslint-disable-next-line no-extend-native + String.prototype.isNotEmpty = function () { + return this.length !== 0; + }; + } }; diff --git a/ui/src/core/helper/debounce.ts b/ui/src/core/helper/debounce.ts new file mode 100644 index 0000000..83ef205 --- /dev/null +++ b/ui/src/core/helper/debounce.ts @@ -0,0 +1,82 @@ +export type Options = { + isImmediate?: boolean; + maxWait?: number; + callback?: (data: Result) => void; +}; + +export interface DebouncedFunction any> { + (this: ThisParameterType, ...args: Args & Parameters): Promise>; + cancel: (reason?: any) => void; +} + +interface DebouncedPromise { + resolve: (result: FunctionReturn) => void; + reject: (reason?: any) => void; +} + +export function debounce any>( + func: F, + waitMilliseconds = 50, + options: Options> = {} +): DebouncedFunction { + let timeoutId: ReturnType | undefined; + const isImmediate = options.isImmediate ?? false; + const callback = options.callback ?? false; + const maxWait = options.maxWait; + let lastInvokeTime = Date.now(); + + let promises: DebouncedPromise>[] = []; + + function nextInvokeTimeout() { + if (maxWait !== undefined) { + const timeSinceLastInvocation = Date.now() - lastInvokeTime; + + if (timeSinceLastInvocation + waitMilliseconds >= maxWait) { + return maxWait - timeSinceLastInvocation; + } + } + + return waitMilliseconds; + } + + const debouncedFunction = function (this: ThisParameterType, ...args: Parameters) { + const context = this; + return new Promise>((resolve, reject) => { + const invokeFunction = function () { + timeoutId = undefined; + lastInvokeTime = Date.now(); + if (!isImmediate) { + const result = func.apply(context, args); + callback && callback(result); + promises.forEach(({ resolve }) => resolve(result)); + promises = []; + } + }; + + const shouldCallNow = isImmediate && timeoutId === undefined; + + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(invokeFunction, nextInvokeTimeout()); + + if (shouldCallNow) { + const result = func.apply(context, args); + callback && callback(result); + return resolve(result); + } + promises.push({ resolve, reject }); + }); + }; + + debouncedFunction.cancel = function (reason?: any) { + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } + promises.forEach(({ reject }) => reject(reason)); + promises = []; + }; + + return debouncedFunction; +} diff --git a/ui/src/core/helper/throttle.ts b/ui/src/core/helper/throttle.ts new file mode 100644 index 0000000..81eb399 --- /dev/null +++ b/ui/src/core/helper/throttle.ts @@ -0,0 +1,29 @@ +export const throttle = ( + fn: (...args: A) => R, + delay: number +): [(...args: A) => R | undefined, () => void] => { + let wait = false; + let timeout: undefined | number; + let cancelled = false; + + return [ + (...args: A) => { + if (cancelled) return undefined; + if (wait) return undefined; + + const val = fn(...args); + + wait = true; + + timeout = window.setTimeout(() => { + wait = false; + }, delay); + + return val; + }, + () => { + cancelled = true; + clearTimeout(timeout); + }, + ]; +}; diff --git a/ui/src/core/model/active_pipiline.ts b/ui/src/core/model/active_pipeline.ts similarity index 79% rename from ui/src/core/model/active_pipiline.ts rename to ui/src/core/model/active_pipeline.ts index 2079613..d375a1a 100644 --- a/ui/src/core/model/active_pipiline.ts +++ b/ui/src/core/model/active_pipeline.ts @@ -1,7 +1,6 @@ - export interface ActivePipeline { pipelineIsRunning: boolean; - projectUUID?: string | null; + projectId?: string | null; lastProcessCompleteCount: number | null; error: any; } diff --git a/ui/src/core/model/ui_base_error.ts b/ui/src/core/model/ui_base_error.ts new file mode 100644 index 0000000..2811b4f --- /dev/null +++ b/ui/src/core/model/ui_base_error.ts @@ -0,0 +1,6 @@ +export class UiBaseError { + text: string; + constructor(text: string) { + this.text = text; + } +} diff --git a/ui/src/core/repository/core_there_repository.ts b/ui/src/core/repository/core_there_repository.ts deleted file mode 100644 index 82b8da9..0000000 --- a/ui/src/core/repository/core_there_repository.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { - DirectionalLight, - Object3D, - PerspectiveCamera, - Scene, - WebGLRenderer, - AmbientLight, - Vector3, - MeshBasicMaterial, - Mesh, - BoxGeometry, - Object3DEventMap, - Box3, - Sphere, - LineBasicMaterial, - EdgesGeometry, - Raycaster, - LineSegments, - Vector2, -} from "three"; -import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; -import { BaseSceneItemModel, StaticAssetItemModel } from "../../features/scene_manager/scene_manager_store"; -import { TypedEvent } from "../helper/typed_event"; -import { Result } from "../helper/result"; - -interface IEmissiveCache { - status: boolean; - object3d: Object3D; -} -export class CoreThereRepository extends TypedEvent { - scene = new Scene(); - camera: PerspectiveCamera; - webGlRender: WebGLRenderer; - htmlCanvasRef: HTMLCanvasElement; - objectEmissive = new Map(); - constructor(htmlCanvasRef: HTMLCanvasElement) { - super(); - const renderer = new WebGLRenderer({ - canvas: htmlCanvasRef as HTMLCanvasElement, - antialias: true, - alpha: true, - }); - const aspectCamera = window.outerWidth / window.outerHeight; - this.camera = new PerspectiveCamera(800, aspectCamera, 0.1, 10000); - this.webGlRender = renderer; - this.htmlCanvasRef = htmlCanvasRef; - this.init(); - } - setRayCastAndGetFirstObject(vector: Vector2): Result { - const raycaster = new Raycaster(); - raycaster.setFromCamera(vector, this.camera); - const intersects = raycaster.intersectObjects(this.scene.children); - if (intersects.length > 0) { - return Result.ok(intersects[0].object.name); - } - - return Result.error(undefined); - } - addCube(num: number, translateTo: string = "X") { - const geometry = new BoxGeometry(1, 1, 1); - const material = new MeshBasicMaterial({ color: 0x00ff00 }); - const cube = new Mesh(geometry, material); - cube.name = "Cube" + String(num); - - eval(`cube.translate${translateTo}(${num * 10})`); - this.scene.add(cube); - } - init() { - const directionalLight = new DirectionalLight(0xffffff, 0.2); - directionalLight.castShadow = true; - directionalLight.position.set(-1, 2, 4); - this.scene.add(directionalLight); - const ambientLight = new AmbientLight(0xffffff, 0.7); - this.scene.add(ambientLight); - - this.addCube(1); - this.addCube(2); - this.addCube(3); - this.addCube(4); - - const onResize = () => { - this.camera.aspect = window.outerWidth / window.outerHeight; - this.camera.updateProjectionMatrix(); - this.webGlRender.setSize(window.outerWidth, window.outerHeight); - }; - window.addEventListener("resize", onResize, false); - new OrbitControls(this.camera, this.htmlCanvasRef); - } - render() { - this.webGlRender.setSize(window.outerWidth, window.outerHeight); - this.webGlRender.setAnimationLoop(() => { - this.webGlRender.render(this.scene, this.camera); - }); - } - getAllSceneModels(): BaseSceneItemModel[] { - return this.getAllSceneNameModels().map((e) => new StaticAssetItemModel(e)); - } - getAllSceneNameModels(): string[] { - return this.scene.children.filter((el) => el.name !== "").map((el) => el.name); - } - getObjectsAtName(name: string): Object3D { - return this.scene.children.filter((el) => el.name === name)[0]; - } - loader(urls: string[], callBack: Function) {} - fitCameraToCenteredObject(objects: string[], offset = 4) { - // https://wejn.org/2020/12/cracking-the-threejs-object-fitting-nut/ - const boundingBox = new Box3().setFromPoints( - objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position) - ); - - var size = new Vector3(); - boundingBox.getSize(size); - - const fov = this.camera.fov * (Math.PI / 180); - const fovh = 2 * Math.atan(Math.tan(fov / 2) * this.camera.aspect); - let dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2)); - let dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2)); - let cameraZ = Math.max(dx, dy); - - if (offset !== undefined && offset !== 0) cameraZ *= offset; - - this.camera.position.set(0, 0, cameraZ); - - const minZ = boundingBox.min.z; - const cameraToFarEdge = minZ < 0 ? -minZ + cameraZ : cameraZ - minZ; - - this.camera.far = cameraToFarEdge * 3; - this.camera.updateProjectionMatrix(); - let orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef); - - orbitControls.maxDistance = cameraToFarEdge * 2; - new OrbitControls(this.camera, this.htmlCanvasRef); - } - switchObjectEmissive(name: string) { - const mesh = this.getObjectsAtName(name); - const result = this.objectEmissive.get(mesh.name); - if (result?.status) { - this.scene.remove(mesh); - this.scene.add(result.object3d); - this.objectEmissive.set(mesh.name, { - status: false, - object3d: mesh, - }); - } else { - this.objectEmissive.set(mesh.name, { - status: true, - object3d: mesh, - }); - - if (mesh instanceof Mesh) { - const newMesh = new LineSegments(mesh.geometry, new LineBasicMaterial({ color: 0x000000 })); - newMesh.name = mesh.name; - newMesh.translateX(mesh.position.x); - newMesh.translateY(mesh.position.y); - newMesh.translateZ(mesh.position.z); - this.scene.remove(mesh); - this.scene.add(newMesh); - } - } - } - - fitSelectedObjectToScreen(objects: string[]) { - //https://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object - let boundBox = new Box3().setFromPoints(objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position)); - let boundSphere = boundBox.getBoundingSphere(new Sphere()); - let vFoV = this.camera.getEffectiveFOV(); - let hFoV = this.camera.fov * this.camera.aspect; - let FoV = Math.min(vFoV, hFoV); - let FoV2 = FoV / 2; - let dir = new Vector3(); - this.camera.getWorldDirection(dir); - let bsWorld = boundSphere.center.clone(); - let th = (FoV2 * Math.PI) / 180.0; - let sina = Math.sin(th); - let R = boundSphere.radius; - let FL = R / sina; - let cameraDir = new Vector3(); - let cameraOffs = cameraDir.clone(); - cameraOffs.multiplyScalar(-FL); - - let newCameraPos = bsWorld.clone().add(cameraOffs); - - this.camera.translateX(newCameraPos.x); - this.camera.translateY(newCameraPos.y); - this.camera.translateZ(newCameraPos.z); - this.camera.lookAt(bsWorld); - new OrbitControls(this.camera, this.htmlCanvasRef); - } -} diff --git a/ui/src/core/repository/core_three_repository.ts b/ui/src/core/repository/core_three_repository.ts new file mode 100644 index 0000000..5034b57 --- /dev/null +++ b/ui/src/core/repository/core_three_repository.ts @@ -0,0 +1,401 @@ +import { + DirectionalLight, + Object3D, + PerspectiveCamera, + Scene, + WebGLRenderer, + AmbientLight, + Vector3, + Mesh, + Object3DEventMap, + Box3, + Sphere, + LineBasicMaterial, + Intersection, + Raycaster, + LineSegments, + Vector2, + Color, + GridHelper, + CameraHelper, + Quaternion, +} from "three"; +import { TypedEvent } from "../helper/typed_event"; +import { Result } from "../helper/result"; +import { GLTFLoader, OrbitControls, TransformControls, OBJLoader, STLLoader, ColladaLoader } from "three-stdlib"; +import { + BaseSceneItemModel, + CameraViewModel, + StaticAssetItemModel, +} from "../../features/scene_manager/model/scene_assets"; +import { SceneMode } from "../../features/scene_manager/model/scene_view"; +import { throttle } from "../helper/throttle"; +import { + InstanceRgbCamera, + RobossemblerAssets, + SceneSimpleObject, +} from "../../features/scene_manager/model/robossembler_assets"; + +export enum UserData { + selectedObject = "selected_object", + cameraInitialization = "camera_initialization", +} + +interface IEventDraggingChange { + target: null; + type: string; + value: boolean; +} + +interface IEmissiveCache { + status: boolean; + object3d: Object3D; +} + +export class CoreThreeRepository extends TypedEvent { + scene = new Scene(); + camera: PerspectiveCamera; + webGlRender: WebGLRenderer; + htmlCanvasRef: HTMLCanvasElement; + objectEmissive = new Map(); + transformControls: TransformControls; + orbitControls: OrbitControls; + htmlSceneWidth: number; + htmlSceneHeight: number; + objLoader = new OBJLoader(); + glbLoader = new GLTFLoader(); + daeLoader = new ColladaLoader(); + stlLoader = new STLLoader(); + watcherSceneEditorObject: Function; + + constructor(htmlCanvasRef: HTMLCanvasElement, watcherSceneEditorObject: Function) { + super(); + this.htmlSceneWidth = window.innerWidth; + this.htmlSceneHeight = window.innerHeight; + this.watcherSceneEditorObject = watcherSceneEditorObject; + const renderer = new WebGLRenderer({ + canvas: htmlCanvasRef as HTMLCanvasElement, + antialias: true, + alpha: true, + }); + const aspectCamera = this.htmlSceneWidth / this.htmlSceneHeight; + this.camera = new PerspectiveCamera(800, aspectCamera, 0.1, 10000); + this.camera.position.set(60, 20, 10); + + this.webGlRender = renderer; + this.htmlCanvasRef = htmlCanvasRef; + + this.transformControls = new TransformControls(this.camera, htmlCanvasRef); + + this.scene.add(this.transformControls); + this.orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef); + this.scene.background = new Color("black"); + + this.init(); + } + + deleteSceneItem(item: BaseSceneItemModel) { + const updateScene = this.scene; + updateScene.children = item.deleteToScene(updateScene); + } + + loadInstances(robossemblerAssets: RobossemblerAssets) { + robossemblerAssets.instances.forEach(async (el) => { + if (el instanceof InstanceRgbCamera) { + const cameraModel = CameraViewModel.fromInstanceRgbCamera(el); + cameraModel.mapPerspectiveCamera(this.htmlSceneWidth, this.htmlSceneHeight).forEach((el) => this.scene.add(el)); + this.emit(cameraModel); + } + if (el instanceof SceneSimpleObject) { + const asset = robossemblerAssets.getAssetAtInstance(el.instanceAt as string); + this.loader( + asset.meshPath, + () => {}, + asset.name, + new Vector3(el.position.x, el.position.y, el.position.z), + new Quaternion(el.quaternion[0], el.quaternion[1], el.quaternion[2], el.quaternion[3]) + ); + } + }); + } + + setTransformMode(mode?: SceneMode) { + switch (mode) { + case undefined: + this.transformControls.detach(); + this.transformControls.dispose(); + break; + case SceneMode.MOVING: + this.transformControls.setMode("translate"); + break; + case SceneMode.ROTATE: + this.transformControls.setMode("rotate"); + break; + } + } + + addSceneCamera(cameraModel: CameraViewModel) { + cameraModel.mapPerspectiveCamera(this.htmlSceneWidth, this.htmlSceneHeight).forEach((el) => this.scene.add(el)); + } + + disposeTransformControlsMode() { + this.transformControls.detach(); + } + + setRayCastAndGetFirstObjectName(vector: Vector2): Result { + this.scene.add(this.transformControls); + const raycaster = new Raycaster(); + raycaster.setFromCamera(vector, this.camera); + const intersects = raycaster.intersectObjects(this.scene.children); + if (intersects.length > 0) { + return Result.ok(intersects[0].object.name); + } + + return Result.error(undefined); + } + + setTransformControlsAttach(object: Object3D) { + if (object instanceof CameraHelper) { + this.transformControls.attach(object.camera); + return; + } + this.transformControls.attach(object); + } + + setRayCastAndGetFirstObject(vector: Vector2): Result> { + try { + const result = this.setRayCast(vector); + return result.fold( + (intersects) => { + const result = intersects.find((element) => element.object.userData[UserData.selectedObject] !== undefined); + if (result === undefined) { + return Result.error(undefined); + } + return Result.ok(result.object); + }, + (_error) => { + return Result.error(undefined); + } + ); + } catch (error) { + return Result.error(undefined); + } + } + + setRayCast(vector: Vector2): Result>[]> { + const raycaster = new Raycaster(); + raycaster.setFromCamera(vector, this.camera); + const intersects = raycaster.intersectObjects(this.scene.children); + if (intersects.length > 0) { + return Result.ok(intersects); + } + + return Result.error(undefined); + } + + setRayCastAndGetFirstObjectAndPointToObject(vector: Vector2): Result { + this.setRayCast(vector).map((intersects) => { + if (intersects.length > 0) { + return Result.ok(intersects[0].point); + } + }); + return Result.error(undefined); + } + + light() { + const directionalLight = new DirectionalLight(0xffffff, 0.2); + directionalLight.castShadow = true; + directionalLight.position.set(-1, 2, 4); + this.scene.add(directionalLight); + const ambientLight = new AmbientLight(0xffffff, 0.7); + this.scene.add(ambientLight); + } + + addListeners() { + window.addEventListener( + "resize", + () => { + this.camera.aspect = this.htmlSceneWidth / this.htmlSceneHeight; + this.camera.updateProjectionMatrix(); + + this.webGlRender.setSize(this.htmlSceneWidth, this.htmlSceneHeight); + }, + false + ); + + this.transformControls.addEventListener("dragging-changed", (event) => { + const e = event as unknown as IEventDraggingChange; + this.orbitControls.enabled = !e.value; + }); + this.transformControls.addEventListener("objectChange", (event) => { + //@ts-expect-error + const sceneObject = event.target.object; + //TODO:(IDONTSUDO) Trotting doesn't work, need to figure out why + const fn = () => this.watcherSceneEditorObject(sceneObject); + const [throttleFn] = throttle(fn, 1000); + throttleFn(); + }); + } + + init() { + this.light(); + this.addListeners(); + const floor = new GridHelper(100, 100, 0x888888, 0x444444); + floor.userData = {}; + floor.userData[UserData.cameraInitialization] = true; + this.scene.add(floor); + } + + render() { + this.webGlRender.setSize(this.htmlSceneWidth, this.htmlSceneHeight); + this.webGlRender.setAnimationLoop(() => { + this.webGlRender.render(this.scene, this.camera); + }); + } + + getAllSceneModels(): BaseSceneItemModel[] { + return this.getAllSceneNameModels().map( + (name) => + new StaticAssetItemModel(name, this.getObjectsAtName(name).position, this.getObjectsAtName(name).quaternion) + ); + } + + getAllSceneNameModels(): string[] { + return this.scene.children.filter((el) => el.name !== "").map((el) => el.name); + } + + getObjectsAtName(name: string): Object3D { + return this.scene.children.filter((el) => el.name === name)[0]; + } + + loader(url: string, callBack: Function, name: string, position?: Vector3, quaternion?: Quaternion) { + const ext = url.split(/\./g).pop()!.toLowerCase(); + + switch (ext) { + case "gltf": + case "glb": + this.glbLoader.load( + url, + (result) => {}, + (err) => {} + ); + break; + case "obj": + this.objLoader.load( + url, + (result) => { + result.userData[UserData.selectedObject] = true; + result.children.forEach((el) => { + el.userData[UserData.selectedObject] = true; + el.name = name; + + if (position) el.position.copy(position); + if (quaternion) el.quaternion.copy(quaternion); + + this.emit(new StaticAssetItemModel(el.name, el.position, el.quaternion)); + this.scene.add(el); + }); + }, + (err) => {} + ); + break; + case "dae": + this.daeLoader.load( + url, + (result) => {}, + (err) => {} + ); + break; + case "stl": + this.stlLoader.load( + url, + (result) => {}, + + (err) => {} + ); + break; + } + } + + fitCameraToCenteredObject(objects: string[], offset = 4) { + const boundingBox = new Box3().setFromPoints( + objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position) + ); + + var size = new Vector3(); + boundingBox.getSize(size); + + const fov = this.camera.fov * (Math.PI / 180); + const fovh = 2 * Math.atan(Math.tan(fov / 2) * this.camera.aspect); + let dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2)); + let dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2)); + let cameraZ = Math.max(dx, dy); + + if (offset !== undefined && offset !== 0) cameraZ *= offset; + + this.camera.position.set(0, 0, cameraZ); + + const minZ = boundingBox.min.z; + const cameraToFarEdge = minZ < 0 ? -minZ + cameraZ : cameraZ - minZ; + + this.camera.far = cameraToFarEdge * 3; + this.camera.updateProjectionMatrix(); + let orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef); + + orbitControls.maxDistance = cameraToFarEdge * 2; + this.orbitControls = orbitControls; + } + + switchObjectEmissive(name: string) { + const mesh = this.getObjectsAtName(name); + const result = this.objectEmissive.get(mesh.name); + if (result?.status) { + this.scene.remove(mesh); + this.scene.add(result.object3d); + this.objectEmissive.set(mesh.name, { + status: false, + object3d: mesh, + }); + } else { + this.objectEmissive.set(mesh.name, { + status: true, + object3d: mesh, + }); + + if (mesh instanceof Mesh) { + const newMesh = new LineSegments(mesh.geometry, new LineBasicMaterial({ color: 0x000000 })); + newMesh.name = mesh.name; + newMesh.position.copy(mesh.position); + + this.scene.remove(mesh); + this.scene.add(newMesh); + } + } + } + + fitSelectedObjectToScreen(objects: string[]) { + //https://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object + let boundBox = new Box3().setFromPoints(objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position)); + let boundSphere = boundBox.getBoundingSphere(new Sphere()); + let vFoV = this.camera.getEffectiveFOV(); + let hFoV = this.camera.fov * this.camera.aspect; + let FoV = Math.min(vFoV, hFoV); + let FoV2 = FoV / 2; + let dir = new Vector3(); + this.camera.getWorldDirection(dir); + let bsWorld = boundSphere.center.clone(); + let th = (FoV2 * Math.PI) / 180.0; + let sina = Math.sin(th); + let R = boundSphere.radius; + let FL = R / sina; + let cameraDir = new Vector3(); + let cameraOffs = cameraDir.clone(); + cameraOffs.multiplyScalar(-FL); + + let newCameraPos = bsWorld.clone().add(cameraOffs); + + this.camera.position.copy(newCameraPos); + this.camera.lookAt(bsWorld); + this.orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef); + } +} diff --git a/ui/src/core/repository/http_repository.ts b/ui/src/core/repository/http_repository.ts index 0ebb2e7..dc6b2ef 100644 --- a/ui/src/core/repository/http_repository.ts +++ b/ui/src/core/repository/http_repository.ts @@ -1,3 +1,4 @@ +import { ClassConstructor, plainToInstance } from "class-transformer"; import { Result } from "../helper/result"; export enum HttpMethod { @@ -16,12 +17,22 @@ export class HttpError extends Error { export class HttpRepository { private server = "http://localhost:4001"; + public async _formDataRequest(method: HttpMethod, url: string, data?: any): Promise> { + let formData = new FormData(); + formData.append("file", data); - public async jsonRequest( - method: HttpMethod, - url: string, - data?: any - ): Promise> { + const reqInit = { + body: formData, + method: method, + }; + + const response = await fetch(this.server + url, reqInit); + if (response.status !== 200) { + throw Result.error(new Error(await response.json())); + } + return Result.ok(response.text as T); + } + public async _jsonRequest(method: HttpMethod, url: string, data?: any): Promise> { try { const reqInit = { body: data, @@ -37,28 +48,50 @@ export class HttpRepository { return Result.error(new HttpError(this.server + url, response.status)); } - return Result.ok(await response.json()); + return Result.ok(await response.json()); } catch (error) { return Result.error(new HttpError(error, 0)); } } - public async request( - method: HttpMethod, - url: string, - data?: any - ): Promise { + public async _request(method: HttpMethod, url: string, data?: any): Promise> { const reqInit = { body: data, method: method, }; + if (data !== undefined) { reqInit["body"] = data; } const response = await fetch(this.server + url, reqInit); if (response.status !== 200) { - throw new Error(await response.json()); + throw Result.error(new Error(await response.json())); + } + return Result.ok(response.text as T); + } + public async _jsonToClassInstanceRequest( + method: HttpMethod, + url: string, + instance: ClassConstructor, + data?: any + ) { + try { + const reqInit = { + body: data, + method: method, + headers: { "Content-Type": "application/json" }, + }; + if (data !== undefined) { + reqInit["body"] = JSON.stringify(data); + } + const response = await fetch(this.server + url, reqInit); + + if (response.status !== 200) { + return Result.error(new HttpError(this.server + url, response.status)); + } + return Result.ok(plainToInstance(instance, await response.json()) as T); + } catch (error) { + return Result.error(new HttpError(error, 0)); } - return response.json(); } } diff --git a/ui/src/core/routers/routers.tsx b/ui/src/core/routers/routers.tsx index e71ed7b..75731d4 100644 --- a/ui/src/core/routers/routers.tsx +++ b/ui/src/core/routers/routers.tsx @@ -21,18 +21,17 @@ import { CreateProcessScreen, CreateProcessScreenPath, } from "../../features/create_process/presentation/create_process_screen"; -import { ProjectRepository } from "../../features/all_projects/data/project_repository"; import { CreateProjectInstancePath, CreateProjectInstanceScreen, } from "../../features/create_project_instance/create_project_instance"; +import { SceneManger, SceneManagerPath } from "../../features/scene_manager/presentation/scene_manager"; const idURL = ":id"; export const router = createBrowserRouter([ { path: AllProjectScreenPath, - loader: new ProjectRepository().loader, element: , }, { @@ -63,4 +62,8 @@ export const router = createBrowserRouter([ path: CreateProjectInstancePath + idURL, element: , }, + { + path: SceneManagerPath + idURL, + element: , + }, ]); diff --git a/ui/src/core/store/base_store.ts b/ui/src/core/store/base_store.ts index 825a76e..f771f62 100644 --- a/ui/src/core/store/base_store.ts +++ b/ui/src/core/store/base_store.ts @@ -1,22 +1,50 @@ // TODO(IDONTSUDO): нужно переписать все запросы под BaseStore +import { NavigateFunction } from "react-router-dom"; import { Result } from "../helper/result"; +import { UiBaseError } from "../model/ui_base_error"; +import { HttpError } from "../repository/http_repository"; -export class BaseStore { +export type CoreError = HttpError | Error; + +export abstract class UiLoader { isLoading = false; - isError = false; - - async loadingHelper(callBack: Promise>) { + async httpHelper(callBack: Promise>) { this.isLoading = true; const result = await callBack; if (result.isFailure()) { - this.isError = true; this.isLoading = false; + this.errorHandingStrategy(result.error); return result.forward(); } this.isLoading = false; return result; } + abstract errorHandingStrategy: (error?: any) => void; + + mapOk = async (property: string, callBack: Promise>) => { + return ( + (await this.httpHelper(callBack)) + // eslint-disable-next-line array-callback-return + .map((el) => { + // @ts-ignore + this[property] = el; + }) + ); + }; +} +export class SimpleErrorState extends UiLoader { + errorHandingStrategy = () => { + this.isError = true; + }; + isError = false; +} + +export abstract class UiErrorState extends UiLoader { + abstract errorHandingStrategy: (error: T) => void; + abstract init(navigate?: NavigateFunction): Promise; + dispose() {} + errors: UiBaseError[] = []; } diff --git a/ui/src/features/all_projects/data/project_repository.ts b/ui/src/features/all_projects/data/project_repository.ts index 19641a9..4cc8038 100644 --- a/ui/src/features/all_projects/data/project_repository.ts +++ b/ui/src/features/all_projects/data/project_repository.ts @@ -1,27 +1,16 @@ -import { redirect } from "react-router-dom"; -import { ActivePipeline } from "../../../core/model/active_pipiline"; -import { - HttpMethod, - HttpRepository, -} from "../../../core/repository/http_repository"; -import { PipelineInstanceScreenPath } from "../../pipeline_instance_main_screen/pipeline_instance_screen"; +import { ActivePipeline } from "../../../core/model/active_pipeline"; +import { HttpMethod, HttpRepository } from "../../../core/repository/http_repository"; import { IProjectModel } from "../model/project_model"; export class ProjectRepository extends HttpRepository { async getAllProject() { - return this.jsonRequest(HttpMethod.GET, "/project"); + return this._jsonRequest(HttpMethod.GET, "/project_instance"); } - + async getActivePipeline() { - return this.jsonRequest(HttpMethod.GET, "/realtime"); + return this._jsonRequest(HttpMethod.GET, "/realtime"); + } + async setActivePipeline(id: string) { + return this._jsonRequest(HttpMethod.POST, `/project_instance/set/active/project?id=${id}`); } - loader = async () => { - const result = await this.getActivePipeline(); - - // if (result.isSuccess() && result.value.projectUUID !== null) { - // return redirect(PipelineInstanceScreenPath + result.value.projectUUID); - // } - - return null; - }; } diff --git a/ui/src/features/all_projects/presentation/all_projects_screen.tsx b/ui/src/features/all_projects/presentation/all_projects_screen.tsx index 6248f6b..92bcea5 100644 --- a/ui/src/features/all_projects/presentation/all_projects_screen.tsx +++ b/ui/src/features/all_projects/presentation/all_projects_screen.tsx @@ -4,13 +4,18 @@ import { ProjectRepository } from "../data/project_repository"; import { LoadPage } from "../../../core/ui/pages/load_page"; import { observer } from "mobx-react-lite"; import { SelectProjectScreenPath } from "../../select_project/presentation/select_project"; +import { useNavigate } from "react-router-dom"; +import { Button } from "antd"; +import { PipelineInstanceScreenPath } from "../../pipeline_instance_main_screen/pipeline_instance_screen"; export const AllProjectScreenPath = "/"; - export const AllProjectScreen: React.FunctionComponent = observer(() => { - const [allProjectStore] = React.useState( - () => new AllProjectStore(new ProjectRepository()) - ); + const [allProjectStore] = React.useState(() => new AllProjectStore(new ProjectRepository())); + const navigate = useNavigate(); + + React.useEffect(() => { + allProjectStore.init(); + }, [allProjectStore]); return ( <> @@ -23,8 +28,33 @@ export const AllProjectScreen: React.FunctionComponent = observer(() => { isLoading={allProjectStore.isLoading} children={
+

Projects

+
+ + {allProjectStore.activePipeline?.projectId ?? "loading"} +
{allProjectStore.projectsModels?.map((el) => { - return
{el.description}
; + return ( +
+ +
{el.description}
+
+ ); })}
} diff --git a/ui/src/features/all_projects/presentation/all_projects_store.ts b/ui/src/features/all_projects/presentation/all_projects_store.ts index 7851c79..7e512dc 100644 --- a/ui/src/features/all_projects/presentation/all_projects_store.ts +++ b/ui/src/features/all_projects/presentation/all_projects_store.ts @@ -1,24 +1,49 @@ import makeAutoObservable from "mobx-store-inheritance"; import { ProjectRepository } from "../data/project_repository"; import { IProjectModel } from "../model/project_model"; -import { BaseStore } from "../../../core/store/base_store"; +import { SimpleErrorState } from "../../../core/store/base_store"; +import { ActivePipeline } from "../../../core/model/active_pipeline"; -export class AllProjectStore extends BaseStore { +interface IProjectView { + isActive: boolean; + description: string; + id: string; +} +export class ProjectView { + isActive: boolean; + description: string; + id: string; + constructor(view: IProjectView) { + this.isActive = view.isActive; + this.description = view.description; + this.id = view.id; + } +} +export class AllProjectStore extends SimpleErrorState { projectsModels?: IProjectModel[]; repository: ProjectRepository; - redirect = false; + activePipeline?: ActivePipeline; constructor(repository: ProjectRepository) { super(); this.repository = repository; makeAutoObservable(this); } - - async getProjects() { - const result = await this.loadingHelper(this.repository.getAllProject()); - if (result.isSuccess()) { - this.projectsModels = result.value; - } + async getProjects(): Promise { + await this.mapOk("projectsModels", this.repository.getAllProject()); } - + async getActiveProject(): Promise { + await this.mapOk("activePipeline", this.repository.getActivePipeline()); + } + + async init() { + await Promise.all([this.getProjects(), this.getActiveProject()]); + await this.projectViewGenerate(); + } + projectViewGenerate() { + this.projectsModels = this.projectsModels?.filter((el) => el._id !== this.activePipeline?.projectId); + } + async setPipelineActive(id: string) { + await this.httpHelper(this.repository.setActivePipeline(id)); + } } diff --git a/ui/src/features/create_pipeline/data/create_pipeline_repository.ts b/ui/src/features/create_pipeline/data/create_pipeline_repository.ts index c12a8f1..7532737 100644 --- a/ui/src/features/create_pipeline/data/create_pipeline_repository.ts +++ b/ui/src/features/create_pipeline/data/create_pipeline_repository.ts @@ -1,26 +1,18 @@ -import { - HttpMethod, - HttpRepository, -} from "../../../core/repository/http_repository"; +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"; import { PipelineModelDataBase } from "../model/pipeline_model"; export class CreatePipelineRepository extends HttpRepository { - async savePipeline( - model: PipelineModelDataBase - ): Promise> { - return await this.jsonRequest(HttpMethod.POST, `/pipeline`, model); + async savePipeline(model: PipelineModelDataBase): Promise> { + return await this._jsonRequest(HttpMethod.POST, `/pipeline`, model); } async getTriggers(page = 1): Promise> { - return await this.jsonRequest(HttpMethod.GET, `/trigger?${page}`); + return await this._jsonRequest(HttpMethod.GET, `/trigger?${page}`); } async getProcessed(page = 1): Promise> { - return await this.jsonRequest( - HttpMethod.GET, - `/process?${page}` - ); + return await this._jsonRequest(HttpMethod.GET, `/process?${page}`); } } diff --git a/ui/src/features/create_pipeline/presentation/create_pipeline_screen.tsx b/ui/src/features/create_pipeline/presentation/create_pipeline_screen.tsx index 2d95a95..aac631a 100644 --- a/ui/src/features/create_pipeline/presentation/create_pipeline_screen.tsx +++ b/ui/src/features/create_pipeline/presentation/create_pipeline_screen.tsx @@ -1,15 +1,20 @@ import * as React from "react"; import { Row, Button } from "antd"; import { LoadPage } from "../../../core/ui/pages/load_page"; -import { createPipelineStore } from "./create_pipeline_store"; + import { observer } from "mobx-react-lite"; import { Icon, List } from "../../../core/ui/list/list"; import { CreateTriggerScreenPath } from "../../create_trigger/presentation/create_trigger_screen"; import { CreateProcessScreenPath } from "../../create_process/presentation/create_process_screen"; +import { CreatePipelineStore } from "./create_pipeline_store"; +import { CreatePipelineRepository } from "../data/create_pipeline_repository"; export const CreatePipelineScreenPath = "/create_pipeline"; export const CreatePipelineScreen: React.FunctionComponent = observer(() => { + const [createPipelineStore] = React.useState(() => new CreatePipelineStore(new CreatePipelineRepository())); + + React.useEffect(() => {}, [createPipelineStore]); return ( <> { icon={Icon.add} />
- + i !== index - ); + this.pipelineViewModels = this.pipelineViewModels.filter((_el, i) => i !== index); } addTrigger(e: string, id: string): void { const lastElement = this.pipelineViewModels.lastElement(); @@ -82,12 +80,8 @@ export class CreatePipelineStore extends BaseStore { message.error("not found pipelines process"); return; } - const triggerId = this.pipelineViewModels.find( - (el) => el.type === Type.TRIGGER - )!.uuid as string; - const processId = this.pipelineViewModels.find( - (el) => el.type === Type.PROCESS - )!.uuid as string; + const triggerId = this.pipelineViewModels.find((el) => el.type === Type.TRIGGER)!.uuid as string; + const processId = this.pipelineViewModels.find((el) => el.type === Type.PROCESS)!.uuid as string; this.repository.savePipeline({ process: processId, @@ -96,33 +90,10 @@ export class CreatePipelineStore extends BaseStore { } async loadProcess() { - this.isLoading = true; - const result = await this.repository.getProcessed(); - result.fold( - (s) => { - this.processModels = s; - }, - (_e) => { - this.isError = true; - } - ); - this.isLoading = false; + this.mapOk("processModels", this.repository.getProcessed()); } async loadTriggers() { - this.isLoading = true; - const result = await this.repository.getTriggers(1); - result.fold( - (s) => { - this.triggersModels = s; - }, - (_e) => { - this.isError = true; - } - ); - this.isLoading = false; + this.mapOk("triggersModels", this.repository.getTriggers()); } } -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 index fa4e44b..5b8f64b 100644 --- a/ui/src/features/create_process/data/process_repostiory.ts +++ b/ui/src/features/create_process/data/process_repostiory.ts @@ -1,11 +1,8 @@ -import { - HttpMethod, - HttpRepository, -} from "../../../core/repository/http_repository"; +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); + await this._jsonRequest(HttpMethod.POST, "/process", model); } } diff --git a/ui/src/features/create_process/presentation/create_process_screen.tsx b/ui/src/features/create_process/presentation/create_process_screen.tsx index 27159ce..0bf13d7 100644 --- a/ui/src/features/create_process/presentation/create_process_screen.tsx +++ b/ui/src/features/create_process/presentation/create_process_screen.tsx @@ -1,18 +1,11 @@ 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 { 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 CreateProcessScreenPath = '/create/process' +export const CreateProcessScreenPath = "/create/process"; export const CreateProcessScreen = observer(() => { return (
@@ -49,22 +42,11 @@ export const CreateProcessScreen = observer(() => { - + - - + + diff --git a/ui/src/features/create_process/presentation/logic/process_store.ts b/ui/src/features/create_process/presentation/logic/process_store.ts index 127cb56..d9e5afc 100644 --- a/ui/src/features/create_process/presentation/logic/process_store.ts +++ b/ui/src/features/create_process/presentation/logic/process_store.ts @@ -8,8 +8,8 @@ class ProcessStore { this.repository = repository; makeAutoObservable(this); } - async saveResult(model:IProcess) { - await this.repository.save(model) + async saveResult(model: IProcess) { + await this.repository.save(model); } } diff --git a/ui/src/features/create_project/create_project_repository.ts b/ui/src/features/create_project/create_project_repository.ts index 325ccfe..68082f3 100644 --- a/ui/src/features/create_project/create_project_repository.ts +++ b/ui/src/features/create_project/create_project_repository.ts @@ -1,10 +1,7 @@ import { Result } from "../../core/helper/result"; import { DatabaseModel } from "../../core/model/database_model"; import { ITriggerModel } from "../../core/model/trigger_model"; -import { - HttpMethod, - HttpRepository, -} from "../../core/repository/http_repository"; +import { HttpMethod, HttpRepository } from "../../core/repository/http_repository"; import { IProcess } from "../create_process/model/process_model"; import { ICreateProjectViewModel } from "./project_model"; @@ -15,11 +12,9 @@ export interface PipelineModel extends DatabaseModel { export class CreateProjectRepository extends HttpRepository { async getAllPipelines(page = 1): Promise> { - return await this.jsonRequest(HttpMethod.GET, "/pipeline"); + return await this._jsonRequest(HttpMethod.GET, "/pipeline"); } - async saveProject( - model: ICreateProjectViewModel - ): Promise> { - return await this.jsonRequest(HttpMethod.POST, "/project", model); + async saveProject(model: ICreateProjectViewModel): Promise> { + return await this._jsonRequest(HttpMethod.POST, "/project", model); } } diff --git a/ui/src/features/create_project/create_project_screen.tsx b/ui/src/features/create_project/create_project_screen.tsx index f12cc10..273193e 100644 --- a/ui/src/features/create_project/create_project_screen.tsx +++ b/ui/src/features/create_project/create_project_screen.tsx @@ -1,17 +1,23 @@ import * as React from "react"; -import { LoadPage } from "../../core/ui/pages/load_page"; -import { createProjectStore } from "./create_project_store"; +import { LoadPage as MainPage } from "../../core/ui/pages/load_page"; import { observer } from "mobx-react-lite"; import { Col, Row, Input, Button } from "antd"; import { ReactComponent as AddIcon } from "../../core/assets/icons/add.svg"; import { CreatePipelineScreenPath } from "../create_pipeline/presentation/create_pipeline_screen"; +import { CreateProjectStore } from "./create_project_store"; +import { CreateProjectRepository } from "./create_project_repository"; export const CreateProjectScreenPath = "/create_project"; export const CreateProjectScreen: React.FunctionComponent = observer(() => { + const [createProjectStore] = React.useState(() => new CreateProjectStore(new CreateProjectRepository())); + + React.useEffect(() => { + createProjectStore.init(); + }, [createProjectStore]); return ( <> - { })} - - createProjectStore.setDescriptionToNewProject( - e.target.value - ) - } + onChange={(e) => createProjectStore.setDescriptionToNewProject(e.target.value)} placeholder="project description" /> - + {createProjectStore.newProjectViews.map((el, index) => { @@ -87,4 +86,3 @@ export const CreateProjectScreen: React.FunctionComponent = observer(() => { ); }); - \ No newline at end of file diff --git a/ui/src/features/create_project/create_project_store.ts b/ui/src/features/create_project/create_project_store.ts index 73f634a..c330d02 100644 --- a/ui/src/features/create_project/create_project_store.ts +++ b/ui/src/features/create_project/create_project_store.ts @@ -1,12 +1,9 @@ import makeAutoObservable from "mobx-store-inheritance"; -import { - CreateProjectRepository, - PipelineModel, -} from "./create_project_repository"; +import { CreateProjectRepository, PipelineModel } from "./create_project_repository"; import { message } from "antd"; -import { BaseStore } from "../../core/store/base_store"; +import { SimpleErrorState } from "../../core/store/base_store"; -class CreateProjectStore extends BaseStore { +export class CreateProjectStore extends SimpleErrorState { repository: CreateProjectRepository; pipelineModels?: PipelineModel[]; @@ -17,25 +14,16 @@ class CreateProjectStore extends BaseStore { super(); this.repository = repository; makeAutoObservable(this); - this.loadPipelines(); } - + async init() { + await this.loadPipelines(); + } async addPipeline(model: PipelineModel) { this.newProjectViews.push(model); } async loadPipelines() { - this.isLoading = true; - const result = await this.repository.getAllPipelines(); - result.fold( - (s) => { - this.pipelineModels = s; - }, - (_e) => { - this.isError = true; - } - ); - this.isLoading = false; + this.mapOk("pipelineModels", this.repository.getAllPipelines()); } setDescriptionToNewProject(value: string): void { @@ -71,7 +59,3 @@ class CreateProjectStore extends BaseStore { ); } } - -export const createProjectStore = new CreateProjectStore( - new CreateProjectRepository() -); diff --git a/ui/src/features/create_project_instance/create_project_instance.tsx b/ui/src/features/create_project_instance/create_project_instance.tsx index cc0463b..3927516 100644 --- a/ui/src/features/create_project_instance/create_project_instance.tsx +++ b/ui/src/features/create_project_instance/create_project_instance.tsx @@ -3,7 +3,8 @@ import { CreateProjectInstanceStore } from "./create_project_instance_store"; import { CreateProjectInstanceRepository } from "./create_project_instance_repository"; import { observer } from "mobx-react-lite"; import { Upload, Button } from "antd"; -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; +import { Input } from "antd"; export const CreateProjectInstancePath = "/create/project/instance/"; @@ -12,16 +13,25 @@ export const CreateProjectInstanceScreen = observer(() => { () => new CreateProjectInstanceStore(new CreateProjectInstanceRepository()) ); const id = useParams().id; - createProjectInstanceStore.getProjectById(id as string) + const navigate = useNavigate(); + + React.useEffect(() => { + createProjectInstanceStore.init(navigate, id as string); + }, [id, createProjectInstanceStore, navigate]); + return ( <> +

project description

+ createProjectInstanceStore.setProjectDescription(e.target.value)}> +

root entity

{ - console.log(e); + createProjectInstanceStore.file = e.file.originFileObj; }} > + ); }); diff --git a/ui/src/features/create_project_instance/create_project_instance_repository.ts b/ui/src/features/create_project_instance/create_project_instance_repository.ts index 6b6d1b9..b835961 100644 --- a/ui/src/features/create_project_instance/create_project_instance_repository.ts +++ b/ui/src/features/create_project_instance/create_project_instance_repository.ts @@ -1,10 +1,16 @@ -import { - HttpMethod, - HttpRepository, -} from "../../core/repository/http_repository"; +import { HttpMethod, HttpRepository } from "../../core/repository/http_repository"; +import { NewProjectModel } from "./new_project_model"; export class CreateProjectInstanceRepository extends HttpRepository { - async getProjectInstance(id: string) { - return await this.jsonRequest(HttpMethod.GET, ""); + async setProjectRootFile(file: File) { + return await this._formDataRequest(HttpMethod.POST, "/project_instance/upload", file); + } + + async createNewProject(project: NewProjectModel) { + return await this._jsonRequest(HttpMethod.POST, "/project_instance", project); + } + + async setActiveProject(id: string) { + return await this._jsonRequest(HttpMethod.POST, `/project_instance/set/active/project?id=${id}`); } } diff --git a/ui/src/features/create_project_instance/create_project_instance_store.ts b/ui/src/features/create_project_instance/create_project_instance_store.ts index be7a346..1730a75 100644 --- a/ui/src/features/create_project_instance/create_project_instance_store.ts +++ b/ui/src/features/create_project_instance/create_project_instance_store.ts @@ -1,18 +1,39 @@ import makeAutoObservable from "mobx-store-inheritance"; -import { BaseStore } from "../../core/store/base_store"; +import { SimpleErrorState } from "../../core/store/base_store"; import { CreateProjectInstanceRepository } from "./create_project_instance_repository"; +import { message } from "antd"; +import { HttpMethod } from "../../core/repository/http_repository"; +import { NavigateFunction } from "react-router-dom"; +import { NewProjectModel } from "./new_project_model"; -export class CreateProjectInstanceStore extends BaseStore { +export class CreateProjectInstanceStore extends SimpleErrorState { + newProjectModel: NewProjectModel; + repository: CreateProjectInstanceRepository; + file?: File; + navigate?: NavigateFunction; constructor(repository: CreateProjectInstanceRepository) { super(); this.repository = repository; makeAutoObservable(this); + this.newProjectModel = NewProjectModel.empty(); } - repository: CreateProjectInstanceRepository; - async getProjectById(id: string) { - const result = await this.loadingHelper(this.repository.getProjectInstance(id)) - if(result.isSuccess()){ - + + setProjectDescription(value: string): void { + this.newProjectModel.description = value; + } + init(navigate: NavigateFunction, projectId: string) { + this.navigate = navigate; + this.newProjectModel.project = projectId; + } + async saveInstance(): Promise { + if (this.file === undefined) { + message.error("Need upload file"); + } else { + if (this.newProjectModel.isValid()) { + await this.repository.createNewProject(this.newProjectModel); + await this.repository.setProjectRootFile(this.file); + if (this.navigate !== undefined) this.navigate("/"); + } } } } diff --git a/ui/src/features/create_project_instance/new_project_model.ts b/ui/src/features/create_project_instance/new_project_model.ts new file mode 100644 index 0000000..805ec64 --- /dev/null +++ b/ui/src/features/create_project_instance/new_project_model.ts @@ -0,0 +1,17 @@ +export class NewProjectModel { + project: string; + description: string; + constructor(project: string, description: string) { + this.project = project; + this.description = description; + } + static empty() { + return new NewProjectModel("", ""); + } + isValid(): boolean { + return this.project.isNotEmpty() && this.description.isNotEmpty(); + } + messages() { + return ""; + } +} diff --git a/ui/src/features/create_trigger/data/trigger_repository.ts b/ui/src/features/create_trigger/data/trigger_repository.ts index 3127310..9cd2b25 100644 --- a/ui/src/features/create_trigger/data/trigger_repository.ts +++ b/ui/src/features/create_trigger/data/trigger_repository.ts @@ -1,11 +1,8 @@ -import { - HttpMethod, - HttpRepository, -} from "../../../core/repository/http_repository"; +import { HttpMethod, HttpRepository } from "../../../core/repository/http_repository"; import { ITriggerModel } from "../../../core/model/trigger_model"; export class TriggerRepository extends HttpRepository { public async save(model: ITriggerModel) { - return await this.jsonRequest(HttpMethod.POST, "/trigger", model); + return await this._jsonRequest(HttpMethod.POST, "/trigger", model); } } diff --git a/ui/src/features/create_trigger/presentation/components/code_trigger_form.tsx b/ui/src/features/create_trigger/presentation/components/code_trigger_form.tsx index 14e7946..d399667 100644 --- a/ui/src/features/create_trigger/presentation/components/code_trigger_form.tsx +++ b/ui/src/features/create_trigger/presentation/components/code_trigger_form.tsx @@ -2,36 +2,40 @@ import * as React from "react"; import Editor from "@monaco-editor/react"; import { Button } from "antd"; import { observer } from "mobx-react-lite"; -import { triggerStore } from "../trigger_store"; +import { CallBackStringVoidFunction } from "../../../../core/extensions/extensions"; -export const CodeTriggerForm: React.FunctionComponent = observer(() => { - return ( - <> -
+interface ICodeTriggerFormProps { + codeTriggerValue: string; + clearTriggerCode: VoidFunction; + saveCode: VoidFunction; + writeNewTrigger: CallBackStringVoidFunction; +} - { - triggerStore.writeNewTrigger(v); - }} - onValidate={(_m) => {}} - /> +export const CodeTriggerForm: React.FunctionComponent = observer( + (props: ICodeTriggerFormProps) => { + return ( + <> +
-
-
+ { + props.writeNewTrigger(v ?? ""); + }} + onValidate={(_m) => {}} + /> - +
+
- - - ); -}); + + + + + ); + } +); diff --git a/ui/src/features/create_trigger/presentation/components/file_trigger_form.tsx b/ui/src/features/create_trigger/presentation/components/file_trigger_form.tsx index a09b70f..2f6936d 100644 --- a/ui/src/features/create_trigger/presentation/components/file_trigger_form.tsx +++ b/ui/src/features/create_trigger/presentation/components/file_trigger_form.tsx @@ -3,50 +3,49 @@ import { Formik } from "formik"; import { SubmitButton, Input, ResetButton, Form, FormItem } from "formik-antd"; import { Row, Col } from "antd"; import { observer } from "mobx-react-lite"; -import { triggerStore } from "../trigger_store"; import { TriggerType } from "../../../../core/model/trigger_model"; import { validateRequired } from "../../../../core/helper/validate"; - -export const FileTriggerForm: React.FunctionComponent = observer(() => { - return ( - <> -
- { - triggerStore.pushTrigger(values.value, TriggerType.FILE); - actions.setSubmitting(false); - actions.resetForm(); - }} - validate={(values) => { - if (values.value.length === 0) { - return false; - } - return {}; - }} - render={() => ( -
-
- - - - - - Reset - Submit - - -
-
- )} - /> -
- - ); -}); +export interface IFileTriggerFormProps { + pushTrigger: (value: string, type: TriggerType) => void; +} +export const FileTriggerForm: React.FunctionComponent = observer( + (props: IFileTriggerFormProps) => { + return ( + <> +
+ { + props.pushTrigger(values.value, TriggerType.FILE); + actions.setSubmitting(false); + actions.resetForm(); + }} + validate={(values) => { + if (values.value.length === 0) { + return false; + } + return {}; + }} + render={() => ( +
+
+ + + + + + Reset + Submit + + +
+
+ )} + /> +
+ + ); + } +); diff --git a/ui/src/features/create_trigger/presentation/create_trigger_screen.tsx b/ui/src/features/create_trigger/presentation/create_trigger_screen.tsx index 73524e2..243f62e 100644 --- a/ui/src/features/create_trigger/presentation/create_trigger_screen.tsx +++ b/ui/src/features/create_trigger/presentation/create_trigger_screen.tsx @@ -2,35 +2,20 @@ import * as React from "react"; import { Button, Col, Row, Switch, Typography, Input } from "antd"; import { CodeTriggerForm } from "./components/code_trigger_form"; import { observer } from "mobx-react-lite"; -import { triggerStore } from "./trigger_store"; import { FileTriggerForm } from "./components/file_trigger_form"; import { ReactComponent as DeleteIcon } from "../../../core/assets/icons/delete.svg"; import { Loader } from "../../../core/ui/loader/loader"; +import { TriggerRepository } from "../data/trigger_repository"; +import { TriggerStore } from "./trigger_store"; +import { TriggerViewModel } from "../model/trigger_form_view_model"; +import { CallBackStringVoidFunction } from "../../../core/extensions/extensions"; const { Title } = Typography; -const Header = observer(() => { - return ( - -
- triggerStore.setTriggerType()} - /> -
- - Trigger editor: {triggerStore.getTriggerDescription()} - -
- -
- ); -}); - -const Bottom = observer(() => { +const Bottom = observer((props: { triggers: TriggerViewModel[]; callBack: CallBackStringVoidFunction }) => { return ( - {triggerStore.triggers.map((el) => { + {props.triggers.map((el) => { return ( { }} > {el.value} - triggerStore.deleteItem(el.id)} /> + props.callBack(el.id)} /> ); })} ); }); -export const CreateTriggerScreenPath = '/create/trigger' +export const CreateTriggerScreenPath = "/create/trigger"; + export const TriggerScreen: React.FunctionComponent = observer(() => { + const [triggerStore] = React.useState(() => new TriggerStore(new TriggerRepository())); return ( <>
{!triggerStore.isLoading ? ( <> -
, - triggerStore.changeTriggerDescription} - /> + +
+ triggerStore.setTriggerType()} /> +
+ Trigger editor: {triggerStore.getTriggerDescription()} +
+ +
+ triggerStore.changeTriggerDescription} /> {triggerStore.getTriggerType() ? ( <> - + ) : ( <> - + )} - + ) : ( <> - - + )}
diff --git a/ui/src/features/create_trigger/presentation/trigger_store.ts b/ui/src/features/create_trigger/presentation/trigger_store.ts index efbfde7..bc251d7 100644 --- a/ui/src/features/create_trigger/presentation/trigger_store.ts +++ b/ui/src/features/create_trigger/presentation/trigger_store.ts @@ -3,9 +3,9 @@ import { v4 as uuidv4 } from "uuid"; import { TriggerType } from "../../../core/model/trigger_model"; import { TriggerRepository } from "../data/trigger_repository"; import { TriggerViewModel } from "../model/trigger_form_view_model"; -import { BaseStore } from "../../../core/store/base_store"; +import { SimpleErrorState } from "../../../core/store/base_store"; -class TriggerStore extends BaseStore { +export class TriggerStore extends SimpleErrorState { constructor(repository: TriggerRepository) { super(); this.triggerType = TriggerType.FILE; @@ -39,9 +39,7 @@ class TriggerStore extends BaseStore { this.triggerType = TriggerType.FILE; }; getTriggerDescription = (): string => { - return this.triggerType === TriggerType.FILE - ? TriggerType.FILE - : TriggerType.PROCESS; + return this.triggerType === TriggerType.FILE ? TriggerType.FILE : TriggerType.PROCESS; }; pushTrigger = (value: string, type: TriggerType): void => { this.triggers.push({ @@ -72,16 +70,15 @@ class TriggerStore extends BaseStore { } } async saveResult(): Promise { - this.isLoading = true; - await this.repository.save({ - type: this.getTriggerDescription(), - description: this.triggerDescription, - value: this.triggers.map((el) => { - return el.value; - }), - }); - this.isLoading = false; + await this.httpHelper( + this.repository.save({ + type: this.getTriggerDescription(), + description: this.triggerDescription, + value: this.triggers.map((el) => { + return el.value; + }), + }) + ); } } - export const triggerStore = new TriggerStore(new TriggerRepository()); diff --git a/ui/src/features/p.tsx b/ui/src/features/p.tsx new file mode 100644 index 0000000..81853f1 --- /dev/null +++ b/ui/src/features/p.tsx @@ -0,0 +1,68 @@ +export {}; +// import React from "react"; +// import { CoreError, UiErrorState } from "../core/store/base_store"; +// import { SelectProjectStore } from "./select_project/presentation/select_project_store"; + +// export declare type ClassConstructor = { +// new (...args: any[]): T; +// }; +// interface MobxReactComponentProps, ClassConstructor> { +// store: ClassConstructor; +// children: (element: T) => React.ReactElement; +// } + +// class UiStateErrorComponent, K> extends React.Component< +// MobxReactComponentProps, +// { store: T | undefined } +// > { +// async componentDidMount(): Promise { +// const store = this.props.store as ClassConstructor; +// console.log(store); +// const s = new store(); +// this.setState({ store: s }); +// if (this.state !== null) { +// await this.state.store?.init(); +// } +// } +// componentWillUnmount(): void { +// if (this.state.store !== undefined) { +// this.state.store.dispose(); +// } +// } + +// render() { +// if (this.state !== null) { +// if (this.state.store?.isLoading) { +// return <>Loading; +// } +// if (this.state.store !== undefined) { +// return this.props.children(this.state.store); +// } +// } + +// return ( +//
+// <>{this.props.children} +//
+// ); +// } +// } + +// export const ExampleScreen: React.FC = () => { +// return ( +//
+// store={SelectProjectStore}> +// {(store) => { +// console.log(store); +// return ( +//
+// {store.projects.map((el) => { +// return <>{el}; +// })} +//
+// ); +// }} +// +//
+// ); +// }; diff --git a/ui/src/features/pipeline_instance_main_screen/pipeline_instance_screen.tsx b/ui/src/features/pipeline_instance_main_screen/pipeline_instance_screen.tsx index 6efdf6d..5476534 100644 --- a/ui/src/features/pipeline_instance_main_screen/pipeline_instance_screen.tsx +++ b/ui/src/features/pipeline_instance_main_screen/pipeline_instance_screen.tsx @@ -4,13 +4,12 @@ import { PipelineInstanceStore } from "./pipeline_instance_store"; export const PipelineInstanceScreenPath = "/pipeline_instance/"; export const PipelineInstanceScreen: React.FunctionComponent = () => { - const [pipelineInstanceStore] = React.useState( - () => new PipelineInstanceStore() - ); - + const [pipelineInstanceStore] = React.useState(() => new PipelineInstanceStore()); + React.useEffect(() => {}, [pipelineInstanceStore]); return (
} diff --git a/ui/src/features/pipeline_instance_main_screen/pipeline_instance_store.ts b/ui/src/features/pipeline_instance_main_screen/pipeline_instance_store.ts index e51c874..7c9bb7f 100644 --- a/ui/src/features/pipeline_instance_main_screen/pipeline_instance_store.ts +++ b/ui/src/features/pipeline_instance_main_screen/pipeline_instance_store.ts @@ -1,7 +1,7 @@ import makeAutoObservable from "mobx-store-inheritance"; -import { BaseStore } from "../../core/store/base_store"; +import { SimpleErrorState } from "../../core/store/base_store"; -export class PipelineInstanceStore extends BaseStore { +export class PipelineInstanceStore extends SimpleErrorState { constructor() { super(); makeAutoObservable(this); diff --git a/ui/src/features/scene_manager/components/static_asset_item_view.tsx b/ui/src/features/scene_manager/components/static_asset_item_view.tsx deleted file mode 100644 index a60272b..0000000 --- a/ui/src/features/scene_manager/components/static_asset_item_view.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from "react"; -import { StaticAssetItemModel } from "../scene_manager_store"; - -export interface IStaticAssetModelViewProps { - model: StaticAssetItemModel; -} - -export function StaticAssetModelView(props: IStaticAssetModelViewProps) { - return ( -
- {props.model.name} -
- ); -} diff --git a/ui/src/features/scene_manager/data/scene_repository.ts b/ui/src/features/scene_manager/data/scene_repository.ts new file mode 100644 index 0000000..30e8575 --- /dev/null +++ b/ui/src/features/scene_manager/data/scene_repository.ts @@ -0,0 +1,19 @@ +import { Result } from "../../../core/helper/result"; +import { HttpMethod, HttpRepository } from "../../../core/repository/http_repository"; +import { CoreError } from "../../../core/store/base_store"; +import { RobossemblerAssets } from "../model/robossembler_assets"; + +export class SceneHttpRepository extends HttpRepository { + async getRobossemblerAssets() { + return this._jsonToClassInstanceRequest( + HttpMethod.GET, + "/robossembler_assets", + RobossemblerAssets + ) as unknown as Promise>; + } + async saveScene(robossemblerAssets: RobossemblerAssets) { + return this._jsonRequest(HttpMethod.POST, "/robossembler_assets", robossemblerAssets) as unknown as Promise< + Result + >; + } +} diff --git a/ui/src/features/scene_manager/model/robossembler_assets.ts b/ui/src/features/scene_manager/model/robossembler_assets.ts new file mode 100644 index 0000000..7c98cd8 --- /dev/null +++ b/ui/src/features/scene_manager/model/robossembler_assets.ts @@ -0,0 +1,160 @@ +import { IsArray, IsEnum, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +export class Gravity { + @IsNumber() + x: number; + @IsNumber() + y: number; + @IsNumber() + z: number; +} + +export class Pose { + @IsNumber() + x: number; + @IsNumber() + y: number; + @IsNumber() + z: number; + @IsNumber() + roll: number; + @IsNumber() + pitch: number; + @IsNumber() + yaw: number; +} + +export class Position { + @IsNumber() + x: number; + @IsNumber() + y: number; + @IsNumber() + z: number; +} + +export enum InstanceType { + RGB_CAMERA = "rgb_camera", + SCENE_SIMPLE_OBJECT = "scene_simple_object", +} + +abstract class CoreInstances {} + +export class Instance extends CoreInstances { + @IsEnum(InstanceType) + instanceType: InstanceType; + @Type(() => Position) + position: Position; + @IsArray() + quaternion: number[]; + @IsOptional() + @IsString() + instanceAt: null | string = null; +} + +export class SceneSimpleObject extends Instance {} + +export class InstanceRgbCamera extends Instance { + @IsString() + cameraLink: string; + @IsString() + topicCameraInfo: string; + @IsOptional() + @IsString() + topicDepth: string | null; + @IsString() + topicImage: string; +} +export class Asset { + @IsString() + name: string; + @IsString() + ixx: string; + @IsString() + ixy: string; + @IsString() + ixz: string; + @IsString() + iyy: string; + @IsString() + izz: string; + @IsString() + mass: string; + @IsString() + posX: string; + @IsString() + posY: string; + @IsString() + posZ: string; + @IsString() + eulerX: string; + @IsString() + eulerY: string; + @IsString() + eulerZ: string; + @IsString() + iyz: string; + @IsString() + meshPath: string; + @IsString() + friction: string; + @IsString() + centerMassX: string; + @IsString() + centerMassY: string; + @IsString() + centerMassZ: string; +} + +export class Physics { + @IsString() + engine_name: string; + @Type(() => Gravity) + gravity: Gravity; +} + +export class RobossemblerAssets { + @ValidateNested() + @Type(() => Asset) + assets: Asset[]; + + @IsArray() + @Type(() => Instance, { + discriminator: { + property: "instanceType", + subTypes: [ + { value: InstanceRgbCamera, name: InstanceType.RGB_CAMERA }, + { value: SceneSimpleObject, name: InstanceType.SCENE_SIMPLE_OBJECT }, + ], + }, + keepDiscriminatorProperty: true, + }) + instances: Instance[]; + + @IsOptional() + @ValidateNested() + @Type(() => Physics) + physics: Physics; + + convertLocalPathsToServerPaths(server_address: string): RobossemblerAssets { + this.assets = this.assets.map((el) => { + el.meshPath = server_address + el.meshPath; + return el; + }); + return this; + } + + getAssetPath(assetName: string): string { + const findElement = this.assets.find((el) => el.name === assetName); + + if (findElement === undefined) { + throw new Error("RobossemblerAssets.getAssetPath not found asset by name:" + assetName); + } + return findElement.meshPath; + } + + getAssetAtInstance(instanceAt: string): Asset { + return this.assets.filter((el) => el.name === instanceAt)[0]; + } +} diff --git a/ui/src/features/scene_manager/model/scene_assets.ts b/ui/src/features/scene_manager/model/scene_assets.ts new file mode 100644 index 0000000..8efc8de --- /dev/null +++ b/ui/src/features/scene_manager/model/scene_assets.ts @@ -0,0 +1,164 @@ +import { CameraHelper, Object3D, PerspectiveCamera, Quaternion, Scene, Vector3 } from "three"; +import { v4 as uuidv4 } from "uuid"; +import { UserData } from "../../../core/repository/core_three_repository"; +import { Asset, Instance, InstanceRgbCamera, InstanceType, SceneSimpleObject } from "./robossembler_assets"; + +export enum RobossemblerFiles { + robossemblerAssets = "robossembler_assets.json", +} + +export enum SceneModelsType { + ASSET, +} + +export abstract class BaseSceneItemModel { + id: string; + name: string; + position: Vector3; + quaternion: Quaternion; + + constructor(name: string) { + this.id = uuidv4(); + this.name = name; + } + + abstract toInstance(): Instance; + + abstract deleteToScene(scene: Scene): Object3D[]; + + toQuaternionArray() { + return [this.quaternion.x, this.quaternion.y, this.quaternion.z, this.quaternion.w]; + } +} + +export class StaticAssetItemModel extends BaseSceneItemModel { + name: string; + type = SceneModelsType.ASSET; + + constructor(name: string, position: Vector3, quaternion: Quaternion) { + super(name); + this.name = name; + this.position = position; + this.quaternion = quaternion; + } + + static fromSceneSimpleObjectAndAsset(sceneSimpleObject: SceneSimpleObject, asset: Asset) { + const { x, y, z } = sceneSimpleObject.position; + const quaternion = sceneSimpleObject.quaternion; + return new StaticAssetItemModel( + asset.name, + new Vector3(x, y, z), + new Quaternion(quaternion[0], quaternion[1], quaternion[2], quaternion[3]) + ); + } + + toInstance(): Instance { + const instance = new Instance(); + instance.instanceType = InstanceType.SCENE_SIMPLE_OBJECT; + instance.position = this.position; + instance.instanceAt = this.name; + instance.quaternion = this.toQuaternionArray(); + + return instance; + } + + deleteToScene(scene: Scene): Object3D[] { + return scene.children.filter((el) => el.name !== this.name); + } +} + +export interface ICoreInstance { + instanceAt: null | string; + type: string; + position: Vector3; + quaternion: Quaternion; +} + +export interface ICameraInstance extends ICoreInstance { + cameraLink: string; + topicImage: string; + topicCameraInfo: string; + topicDepth: string | null; +} + +export class CameraViewModel extends BaseSceneItemModel { + cameraLink: string; + topicImage: string; + topicCameraInfo: string; + topicDepth: null | string; + + constructor(cameraLink: string, topicImage: string, topicCameraInfo: string, topicDepth: string | null) { + super(cameraLink); + this.cameraLink = cameraLink; + this.topicImage = topicImage; + this.topicCameraInfo = topicCameraInfo; + this.topicDepth = topicDepth; + } + + static fromInstanceRgbCamera(instanceRgbCamera: InstanceRgbCamera) { + const { cameraLink, topicImage, topicCameraInfo, topicDepth, position, quaternion } = instanceRgbCamera; + const instance = new CameraViewModel(cameraLink, topicImage, topicCameraInfo, topicDepth); + const { x, y, z } = position; + + instance.position = new Vector3(x, y, z); + instance.quaternion = new Quaternion(quaternion[0], quaternion[1], quaternion[2], quaternion[3]); + + return instance; + } + + deleteToScene(scene: Scene): Object3D[] { + return scene.children.filter((el) => { + if (el.name === this.name) { + return false; + } + if (el.name === this.cameraLink + "camera_helper") { + return false; + } + return true; + }); + } + + toInstance(): InstanceRgbCamera { + return { + instanceType: InstanceType.RGB_CAMERA, + position: this.position, + quaternion: this.toQuaternionArray(), + instanceAt: null, + cameraLink: this.cameraLink, + topicCameraInfo: this.topicCameraInfo, + topicDepth: this.topicDepth, + topicImage: this.topicImage, + }; + } + + validate(cameraLinksNames: string[]) { + if (cameraLinksNames.filter((el) => this.cameraLink === el).isNotEmpty()) { + return { cameraLink: "the name for the camera is not unique" }; + } + if (this.cameraLink.isEmpty()) { + return { cameraLink: "is empty" }; + } + if (this.topicImage.isEmpty()) { + return { topicImage: "is empty" }; + } + if (this.topicCameraInfo.isEmpty()) { + return { topicCameraInfo: "is empty" }; + } + } + + mapPerspectiveCamera(htmlSceneWidth: number, htmlSceneHeight: number) { + const perspectiveCamera = new PerspectiveCamera(48, htmlSceneWidth / htmlSceneHeight, 7.1, 28.5); + perspectiveCamera.position.copy(this.position); + perspectiveCamera.quaternion.copy(this.quaternion); + perspectiveCamera.name = this.cameraLink; + const cameraHelper = new CameraHelper(perspectiveCamera); + perspectiveCamera.userData[UserData.selectedObject] = true; + cameraHelper.userData[UserData.selectedObject] = true; + cameraHelper.name = this.cameraLink + "camera_helper"; + return [cameraHelper, perspectiveCamera]; + } + + static empty() { + return new CameraViewModel("", "", "", ""); + } +} diff --git a/ui/src/features/scene_manager/model/scene_view.ts b/ui/src/features/scene_manager/model/scene_view.ts new file mode 100644 index 0000000..2265a58 --- /dev/null +++ b/ui/src/features/scene_manager/model/scene_view.ts @@ -0,0 +1,27 @@ +export class SceneMenu { + x?: number; + y?: number; + isShow?: boolean; + + constructor(x: number | undefined, y: number | undefined, isShow: boolean | undefined) { + this.x = x; + this.y = y; + this.isShow = isShow; + } + + static empty() { + return new SceneMenu(undefined, undefined, false); + } +} + +export interface SceneManagerView { + name: string; + clickHandel: Function; +} + +export enum SceneMode { + ROTATE = "Rotate", + MOVING = "Moving", + EMPTY = "Empty", + ADD_CAMERA = "Add camera", +} diff --git a/ui/src/features/scene_manager/presentation/components/scene_menu.tsx b/ui/src/features/scene_manager/presentation/components/scene_menu.tsx new file mode 100644 index 0000000..3504579 --- /dev/null +++ b/ui/src/features/scene_manager/presentation/components/scene_menu.tsx @@ -0,0 +1,27 @@ +interface SceneMenuProps { + x?: number; + y?: number; +} + +export const SceneMenu = (props: SceneMenuProps) => { + const sceneMenuStyle = { + transform: "rotate(0deg)", + background: "rgb(73 73 73)", + color: "#FFFFFF", + fontSize: "20px", + display: "flex", + justifyContent: "center", + alignItems: "center", + borderColor: "aqua", + border: "solid", + borderRadius: "30px", + width: "150px", + height: "300px", + }; + + return ( +
+
+
+ ); +}; diff --git a/ui/src/features/scene_manager/presentation/components/scene_widget.tsx b/ui/src/features/scene_manager/presentation/components/scene_widget.tsx new file mode 100644 index 0000000..9cf137b --- /dev/null +++ b/ui/src/features/scene_manager/presentation/components/scene_widget.tsx @@ -0,0 +1,68 @@ +import React from "react"; + +export const SceneWidget = () => { + const [pressed, setPressed] = React.useState(false); + const [position, setPosition] = React.useState({ x: 0, y: 0 }); + + React.useEffect(() => { + if (pressed) { + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("mouseup", togglePressed); + } + + return () => { + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", togglePressed); + }; + }, [position, pressed]); + + const onMouseMove = (event: any) => { + const x = position.x + event.movementX; + const y = position.y + event.movementY; + setPosition({ x, y }); + }; + + const togglePressed = () => { + setPressed((prev) => !prev); + }; + + const quickAndDirtyStyle = { + transform: "rotate(0deg)", + background: "rgb(73 73 73)", + color: "#FFFFFF", + fontSize: "20px", + display: "flex", + justifyContent: "center", + alignItems: "center", + borderColor: "aqua", + border: "solid", + borderRadius: "30px", + }; + return ( +
+
{ + event.stopPropagation(); + setPressed(false); + }} + onMouseDown={togglePressed} + > +

{pressed ? "Dragging..." : "Press to drag"}

+

{ + event.stopPropagation(); + console.log(201); + }} + > + HYO +

+
+
+ ); +}; diff --git a/ui/src/features/scene_manager/presentation/components/static_asset_item_view.tsx b/ui/src/features/scene_manager/presentation/components/static_asset_item_view.tsx new file mode 100644 index 0000000..040faba --- /dev/null +++ b/ui/src/features/scene_manager/presentation/components/static_asset_item_view.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { BaseSceneItemModel, CameraViewModel, StaticAssetItemModel } from "../../model/scene_assets"; +import { Button } from "antd"; + +export interface IStaticAssetModelViewProps { + model: BaseSceneItemModel; + onTap: Function; +} + +export function StaticAssetModelView(props: IStaticAssetModelViewProps) { + if (props.model instanceof CameraViewModel) { + return ( +
+ {props.model.cameraLink} + +
+ ); + } + + if (props.model instanceof StaticAssetItemModel) { + return ( +
+ {props.model.name} + +
+ ); + } + return <>; +} diff --git a/ui/src/features/scene_manager/presentation/scene_manager.tsx b/ui/src/features/scene_manager/presentation/scene_manager.tsx new file mode 100644 index 0000000..bb774c3 --- /dev/null +++ b/ui/src/features/scene_manager/presentation/scene_manager.tsx @@ -0,0 +1,162 @@ +import * as React from "react"; +import { SceneMangerStore } from "./scene_manager_store"; +import { observer } from "mobx-react-lite"; +import { StaticAssetModelView } from "./components/static_asset_item_view"; +import { useParams } from "react-router-dom"; +import { SceneManagerView, SceneMode } from "../model/scene_view"; +import { Button } from "antd"; +import { Form, Input, ResetButton, SubmitButton } from "formik-antd"; +import { Formik } from "formik"; +import { CameraViewModel } from "../model/scene_assets"; + +export const SceneManagerPath = "/scene/manager/"; + +export const SceneManger = observer(() => { + const canvasRef = React.useRef(null); + const [sceneMangerStore] = React.useState(() => new SceneMangerStore()); + const id = useParams().id as string; + + React.useEffect(() => { + sceneMangerStore.init(); + sceneMangerStore.loadScene(id, canvasRef.current!); + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = "scroll"; + sceneMangerStore.dispose(); + }; + }, [id, sceneMangerStore]); + + const sceneIcons: SceneManagerView[] = [SceneMode.ROTATE, SceneMode.MOVING, SceneMode.ADD_CAMERA].map((el) => { + return { name: el, clickHandel: () => sceneMangerStore.setSceneMode(el) }; + }); + + return ( +
+ +
+
+ {sceneIcons.map((el) => { + return ( +
{ + el.clickHandel(); + }} + > + {el.name} +
+ ); + })} +
+
Scene manager
+ {sceneMangerStore.isVisibleSaveButton ? ( + <> + + + ) : ( + <> + )} + {sceneMangerStore.isLoading ? <>Loading... : <>} + {sceneMangerStore.sceneMode === SceneMode.ADD_CAMERA ? ( +
+ { + sceneMangerStore.addNewCamera(model); + actions.setSubmitting(false); + actions.resetForm(); + }} + validate={(model) => { + return model.validate(sceneMangerStore.getCameraLinkNames()); + }} + render={() => ( +
+
+ + + + + + Reset + Submit +
+
+ )} + /> +
+ ) : ( + <> + )} + {sceneMangerStore.sceneMode === SceneMode.MOVING || SceneMode.ROTATE ? ( + <> + {sceneMangerStore.robossemblerAssets?.assets.map((el) => { + return ( +
+
+ {el.name} + {sceneMangerStore.isRenderedAsset(el.name) ? ( + <> + ) : ( + + )} +
+
+ ); + })} + + ) : ( + <> + )} +
+
+
+ {sceneMangerStore.sceneModels.map((el) => { + return sceneMangerStore.deleteSceneItem(el)} model={el} />; + })} +
+ + {/* {sceneMangerStore.sceneMenuIsShow ? ( + <> + + + ) : ( + <> + )} */} +
+
+ ); +}); diff --git a/ui/src/features/scene_manager/presentation/scene_manager_store.ts b/ui/src/features/scene_manager/presentation/scene_manager_store.ts new file mode 100644 index 0000000..78df48f --- /dev/null +++ b/ui/src/features/scene_manager/presentation/scene_manager_store.ts @@ -0,0 +1,179 @@ +import makeAutoObservable from "mobx-store-inheritance"; +import { CoreThreeRepository } from "../../../core/repository/core_three_repository"; +import { Object3D, Vector2 } from "three"; +import { HttpError } from "../../../core/repository/http_repository"; +import { UiErrorState } from "../../../core/store/base_store"; +import { UiBaseError } from "../../../core/model/ui_base_error"; +import { SceneMenu, SceneMode } from "../model/scene_view"; +import { BaseSceneItemModel, CameraViewModel, RobossemblerFiles, StaticAssetItemModel } from "../model/scene_assets"; +import { SceneHttpRepository } from "../data/scene_repository"; +import { message } from "antd"; +import { RobossemblerAssets } from "../model/robossembler_assets"; + +export class SceneMangerStore extends UiErrorState { + sceneMode: SceneMode; + sceneMenu: SceneMenu; + isVisibleSaveButton: boolean = false; + coreThereRepository: null | CoreThreeRepository = null; + sceneHttpRepository: SceneHttpRepository; + sceneModels: BaseSceneItemModel[] = []; + isSceneMenuShow = false; + robossemblerAssets?: RobossemblerAssets; + + constructor() { + super(); + makeAutoObservable(this); + this.sceneHttpRepository = new SceneHttpRepository(); + this.sceneMode = SceneMode.EMPTY; + this.sceneMenu = SceneMenu.empty(); + } + + onTapSave(): void { + this.robossemblerAssets!.instances = []; + this.sceneModels.forEach((el) => this.robossemblerAssets?.instances.push(el.toInstance())); + this.httpHelper(this.sceneHttpRepository.saveScene(this.robossemblerAssets as RobossemblerAssets)); + this.isVisibleSaveButton = false; + } + + deleteSceneItem(item: BaseSceneItemModel) { + const itm = this.sceneModels.filter((el) => el.id === item.id); + this.coreThereRepository!.deleteSceneItem(itm[0]); + this.sceneModels = this.sceneModels.filter((el) => el.name !== item.name); + this.visibleSaveButton(); + } + + visibleSaveButton() { + this.isVisibleSaveButton = true; + } + + addNewCamera(model: CameraViewModel) { + model.position = this.coreThereRepository!.camera.position; + model.quaternion = this.coreThereRepository!.camera.quaternion; + this.sceneModels.push(model); + this.coreThereRepository?.addSceneCamera(model); + this.visibleSaveButton(); + } + + getCameraLinkNames(): string[] { + return this.sceneModels.map((el) => { + if (el instanceof CameraViewModel) { + return el.cameraLink; + } + return ""; + }); + } + + loaderWatcher() {} + + loadSceneRobossemblerAsset(name: string) { + try { + const assetPath = this.robossemblerAssets?.getAssetPath(name) as string; + this.coreThereRepository?.loader(assetPath, this.loaderWatcher, name); + this.visibleSaveButton(); + } catch (error) { + message.error(String(error)); + } + } + + isRenderedAsset(name: string): boolean { + return this.sceneModels + .filter((el) => { + if (el instanceof StaticAssetItemModel) { + return el.name === name; + } + return false; + }) + .isNotEmpty(); + } + + hiddenMenu() { + this.isSceneMenuShow = false; + } + + setSceneMode = (mode: SceneMode) => { + if (this.sceneMode === undefined || this.sceneMode !== mode) { + this.sceneMode = mode; + } else if (this.sceneMode === mode) { + this.sceneMode = SceneMode.EMPTY; + } + this.coreThereRepository?.setTransformMode(this.sceneMode); + this.sceneModeWatcher(); + }; + + sceneModeWatcher() {} + + async init(): Promise {} + + errorHandingStrategy = (error: HttpError) => { + if (error.status === 404) { + this.errors.push(new UiBaseError(`${RobossemblerFiles.robossemblerAssets} not found to project`)); + } + }; + + async loadScene(sceneId: string, canvasRef: HTMLCanvasElement) { + this.loadWebGl(canvasRef); + await this.mapOk("robossemblerAssets", this.sceneHttpRepository.getRobossemblerAssets()); + if (this.robossemblerAssets) { + this.coreThereRepository?.loadInstances(this.robossemblerAssets); + } + } + + loadWebGl(canvasRef: HTMLCanvasElement): void { + this.coreThereRepository = new CoreThreeRepository(canvasRef as HTMLCanvasElement, this.watcherSceneEditorObject); + this.coreThereRepository.on(this.watcherThereObjects); + this.coreThereRepository.render(); + this.sceneModels = this.coreThereRepository.getAllSceneModels(); + + window.addEventListener("click", (event) => this.clickLister(event)); + window.addEventListener("mousedown", (e) => this.sceneContextMenu(e)); + } + + clickLister(event: MouseEvent) { + if (this.sceneMode === SceneMode.EMPTY) { + return; + } + if (this.sceneMode === SceneMode.MOVING || this.sceneMode === SceneMode.ROTATE) { + const vector = new Vector2(); + vector.x = (event.clientX / window.innerWidth) * 2 - 1; + vector.y = -(event.clientY / window.innerHeight) * 2 + 1; + + this.transformContollsCall(vector); + } + } + + sceneContextMenu(e: MouseEvent): void { + if (e.button === 2) { + this.isSceneMenuShow = true; + this.sceneMenu.x = e.clientX; + this.sceneMenu.y = e.clientY; + } + } + + watcherThereObjects = (sceneItemModel: BaseSceneItemModel): void => { + this.sceneModels.push(sceneItemModel); + }; + + watcherSceneEditorObject = (mesh: Object3D) => { + this.sceneModels = this.sceneModels.map((el) => { + if (el.name === mesh.name) { + el.position = mesh.position; + el.quaternion = mesh.quaternion; + return el; + } + return el; + }); + this.visibleSaveButton(); + }; + + transformContollsCall = (vector: Vector2) => { + this.coreThereRepository?.setRayCastAndGetFirstObject(vector).fold( + (success) => this.coreThereRepository?.setTransformControlsAttach(success), + (_error) => this.coreThereRepository?.disposeTransformControlsMode() + ); + }; + + dispose() { + window.removeEventListener("click", this.clickLister); + window.removeEventListener("mousedown", (e) => this.sceneContextMenu(e)); + } +} diff --git a/ui/src/features/scene_manager/scene_manager.tsx b/ui/src/features/scene_manager/scene_manager.tsx deleted file mode 100644 index d3e03b7..0000000 --- a/ui/src/features/scene_manager/scene_manager.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from "react"; -import { SceneMangerStore, StaticAssetItemModel } from "./scene_manager_store"; -import { observer } from "mobx-react-lite"; -import { StaticAssetModelView } from "./components/static_asset_item_view"; - -// const useKeyLister = (fn: Function) => { -// const pressed = new Map(); - -// const registerKeyPress = React.useCallback( -// (event: KeyboardEvent, codes: string[], callBack: Function) => { -// if (codes.hasIncludeElement(event.code)) { -// pressed.addValueOrMakeCallback(event.code, event.type, (e) => { -// if (Array.from(pressed.values()).equals(["keydown", "keydown"], false)) { -// callBack(); -// } -// }); -// } -// }, -// [pressed] -// ); - -// React.useEffect(() => { -// window.addEventListener("keyup", (e) => registerKeyPress(e, ["KeyQ", "KeyW"], () => fn)); -// window.addEventListener("keydown", (e) => registerKeyPress(e, ["KeyQ", "KeyW"], () => {})); -// }, [fn, registerKeyPress]); - -// return []; -// }; - -export const SceneManger = observer(() => { - const canvasRef = React.useRef(null); - - const [sceneMangerStore] = React.useState(() => new SceneMangerStore()); - - React.useEffect(() => { - sceneMangerStore.loadGl(canvasRef.current!); - return () => { - sceneMangerStore.dispose(); - }; - }); - - return ( -
-
- {sceneMangerStore.sceneItems.map((el) => { - if (el instanceof StaticAssetItemModel) { - return StaticAssetModelView({ model: el }); - } - return <>; - })} -
- -
- ); -}); diff --git a/ui/src/features/scene_manager/scene_manager_store.ts b/ui/src/features/scene_manager/scene_manager_store.ts deleted file mode 100644 index 96ae25e..0000000 --- a/ui/src/features/scene_manager/scene_manager_store.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-disable array-callback-return */ -import { makeAutoObservable } from "mobx"; -import { CoreThereRepository } from "../../core/repository/core_there_repository"; -import { v4 as uuidv4 } from "uuid"; -import { Vector2 } from "three"; - -export class BaseSceneItemModel { - id: string; - constructor() { - this.id = uuidv4(); - } -} - -enum SceneModelsType { - ASSET, -} - -export class StaticAssetItemModel extends BaseSceneItemModel { - name: string; - type = SceneModelsType.ASSET; - constructor(name: string) { - super(); - this.name = name; - } -} - -export class SceneMangerStore { - coreThereRepository: null | CoreThereRepository = null; - sceneItems: BaseSceneItemModel[] = []; - - constructor() { - makeAutoObservable(this); - } - - loadGl(canvasRef: HTMLCanvasElement): void { - this.coreThereRepository = new CoreThereRepository(canvasRef as HTMLCanvasElement); - this.coreThereRepository.on(this.watcherThereObjects); - this.coreThereRepository.render(); - this.coreThereRepository.fitCameraToCenteredObject(this.coreThereRepository.getAllSceneNameModels()); - - this.sceneItems = this.coreThereRepository.getAllSceneModels(); - - window.addEventListener("click", this.handleMouseClick); - } - - watcherThereObjects(sceneItemModel: BaseSceneItemModel): void { - this.sceneItems.push(sceneItemModel); - } - - handleMouseClick = (event: MouseEvent) => { - const vector = new Vector2(); - console.log("===="); - console.log(event.pageX); - console.log(event.clientX); - console.log(event.x); - console.log(event.movementX); - console.log(event.screenX); - console.log("===="); - vector.x = (event.clientX / window.innerWidth) * 2 - 1; - vector.y = -(event.clientY / window.innerHeight) * 2 + 1; - - this.coreThereRepository?.setRayCastAndGetFirstObject(vector).map((el) => { - this.coreThereRepository?.switchObjectEmissive(el); - }); - }; - - dispose() { - window.removeEventListener("click", this.handleMouseClick); - } -} diff --git a/ui/src/features/select_project/data/select_project_repository.ts b/ui/src/features/select_project/data/select_project_repository.ts index 67ee41f..16607a5 100644 --- a/ui/src/features/select_project/data/select_project_repository.ts +++ b/ui/src/features/select_project/data/select_project_repository.ts @@ -1,12 +1,12 @@ import { Result } from "../../../core/helper/result"; -import { - HttpMethod, - HttpRepository, -} from "../../../core/repository/http_repository"; +import { HttpMethod, HttpRepository } from "../../../core/repository/http_repository"; import { IProjectModel } from "../model/project_model"; export class SelectProjectRepository extends HttpRepository { + async setActiveProject(id: string) { + return await this._jsonRequest(HttpMethod.POST, `/project?${id}`); + } async getAllProjects(page = 1): Promise> { - return await this.jsonRequest(HttpMethod.GET, `/project?${page}`); + return await this._jsonRequest(HttpMethod.GET, `/project?${page}`); } } diff --git a/ui/src/features/select_project/presentation/select_project.tsx b/ui/src/features/select_project/presentation/select_project.tsx index 543820d..c4e29cc 100644 --- a/ui/src/features/select_project/presentation/select_project.tsx +++ b/ui/src/features/select_project/presentation/select_project.tsx @@ -3,18 +3,19 @@ import { observer } from "mobx-react-lite"; import { LoadPage } from "../../../core/ui/pages/load_page"; import { CreateProjectScreenPath } from "../../create_project/create_project_screen"; import { SelectProjectStore } from "./select_project_store"; -import { SelectProjectRepository } from "../data/select_project_repository"; import { useNavigate } from "react-router-dom"; import { CreateProjectInstancePath } from "../../create_project_instance/create_project_instance"; import { Button } from "antd"; export const SelectProjectScreenPath = "/select_project"; export const SelectProjectScreen: React.FunctionComponent = observer(() => { - const [selectProjectStore] = React.useState( - () => new SelectProjectStore(new SelectProjectRepository()) - ); + const [selectProjectStore] = React.useState(() => new SelectProjectStore()); const navigate = useNavigate(); + React.useEffect(() => { + selectProjectStore.init(); + }, [selectProjectStore, navigate]); + return ( <> { largeText={"Select project"} minText={"add new project?"} isLoading={selectProjectStore.isLoading} - isError={selectProjectStore.isError} + isError={selectProjectStore.errors.isNotEmpty()} children={selectProjectStore.projects.map((el) => { 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 index e950a7f..dd210dd 100644 --- a/ui/src/features/select_project/presentation/select_project_store.ts +++ b/ui/src/features/select_project/presentation/select_project_store.ts @@ -1,33 +1,28 @@ import makeAutoObservable from "mobx-store-inheritance"; import { SelectProjectRepository } from "../data/select_project_repository"; import { IProjectModel } from "../model/project_model"; -import { BaseStore } from "../../../core/store/base_store"; +import { CoreError, UiErrorState } from "../../../core/store/base_store"; -export class SelectProjectStore extends BaseStore { +export class SelectProjectStore extends UiErrorState { + errorHandingStrategy = (error: CoreError) => {}; repository: SelectProjectRepository; - + errors = []; page = 1; projects: IProjectModel[] = []; - constructor(repository: SelectProjectRepository) { - super() - this.repository = repository; + constructor() { + super(); + + this.repository = new SelectProjectRepository(); makeAutoObservable(this); - this.getPipelines(); } - + async setActiveProject(id: string): Promise { + this.httpHelper(this.repository.setActiveProject(id)); + } 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; + await this.mapOk("projects", this.repository.getAllProjects(this.page)); + } + async init() { + await this.getPipelines(); } } - \ No newline at end of file diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 571cfd8..574c7f8 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -1,22 +1,26 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import "reflect-metadata"; 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"; -import { SceneManger } from "./features/scene_manager/scene_manager"; +import { SocketLister } from "./features/socket_lister/socket_lister"; +import { RouterProvider } from "react-router-dom"; +import { router } from "./core/routers/routers"; +import { SceneManger } from "./features/scene_manager/presentation/scene_manager"; extensions(); const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); root.render( <> - {/* - - */} - + {/* */} + {/* */} + + {/* */} + <> + + ); diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 397b455..f13dfaf 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -15,7 +15,10 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "useDefineForClassFields": true + "useDefineForClassFields": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strictPropertyInitialization": false }, "include": ["src"] }