diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2c7bb93..3b2eace 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: [fosslife] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: [fosslife, sparkenstein] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username diff --git a/README.md b/README.md index 7e21cfc..35a4f30 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ So here's DevTools-X -- an x-platform collection of dev-tools that is lighter, s ![GitHub issues](https://badgen.net/github/issues/fosslife/devtools-x) ![GitHub stars](https://badgen.net/github/stars/fosslife/devtools-x) ![Latest release](https://badgen.net/github/release/fosslife/devtools-x) +## Screenshot + +Screenshot 2024-04-01 at 12 20 37 PM + ## Installation Download the relevant package from Github Releases section, and start using it! :D @@ -40,42 +44,48 @@ This project exists solely because I was fed up switching between different tool ## Features -DevTools-X has about **32 features** as of now, and growing. The full list in below, details of each are mentioned in a separate file. One big selling point of DevTools-X is it uses `monaco-editor`, the editor used by vscode, so tons of editor features are -available to you right from the start, as if you are using vscode. And in the backend we use `rust` so large number of heavy duty operations are given to rust to get it done quickly. - -- JSON Editor/Formatter/Fixer/Minifier/Beautifier -- Text Hashes -- Hashing Files -- Password Generator -- JWT Formatter/Parser -- Number Convertor Binary/Hex/etc -- SQL Formatter -- Color Generator/Picker/Convertor -- Regex Tester -- Text/Code Diffing With Syntax Highlighting -- YAML <> JSON Convertor -- Pastebin (Github Gists) -- REST API Tester -- Programming Scratchpad -- Beautiful Markdown Preview -- Image Compressor/Convertor with Preview -- Bulk Image Compressor with Rust for Speed -- Unit Convertor (All Major Units Supported) -- React Scratchpad (Live React Editor to Get Preview) -- Unix EPOCH Convertor -- Stateless Password Generator/Manager -- BASE64 Text Convertor -- BASE64 Image Convertor -- Generating Structs/Types from JSON -- CSS/JS/HTML Minifier/Beautifier -- URL Parser -- HTML Preview -- Lorem Ipsum Sample Text Generator -- QR Code Generator -- PDF Reader -- Ping Command Preview -- Text Compressor -- And Many More Coming +#### Checkout [features.md](features.md) for a short video demo on every feature. + +DevTools-X has about **34 features** as of now, and growing. + +The full list in below, One big selling point of DevTools-X is it uses `monaco-editor`, the editor used by vscode, so tons of editor features are +available to you right from the start, as if you are using vscode. + +1. Basic REST client +2. Unix epoch timestamp convertor +3. Graphical ping +4. Strong password generator +5. QR code generator +6. Code format/minify tools +7. React live scratchpad +8. Lorem Ipsum text generator +9. Image compressor/convertor with preview +10. Pastebin with gist +11. Programming scratchpad with many languages support +12. Bulk image compressor with Rust SIMD +13. Base64 text encode/decode +14. Base64 image encode/decode +15. Text hash calculate (md5, sha etc) +16. Files MD5 +17. JSON formatter/minify etc +18. JWT decode +19. Number convertor +20. SQL formatter +21. Color convertor/picker +22. Code/text diff with syntax highlight +23. Markdown edit/preview +24. YAML JSON convertor +25. Multiple units convertor (length/pressure whatnot) +26. Text gzip/deflate/zlib compression +27. Stateless password generator +28. Generate programming Types and Interfaces from json +29. URL Parser +30. HTML editor and preview +31. PDF Reader +32. Cron edit and explain +33. UUID generator +34. Regex Tester +35. Generate mock data with Faker ## Contributing @@ -102,14 +112,23 @@ That should be enough to tell you it's built on top of [Tauri](https://tauri.app ## FAQ -#### What's up with the bad looking UI? +#### Migrate settings? + +There's a backup/restore feature available in settings drawer. you can backup manually as well, copy `settings.json` from [appDir](https://tauri.app/v1/api/js/path#appdatadir) + +#### App is not starting/showing empty screen -Well, it was even worse previously! I am not a UI developer. I understand React, but not colors. -Feel free to contribute any changes that you think might make it look better. +Most likely your db is corrupt. delete `settings.json` file in your [appDir](https://tauri.app/v1/api/js/path#appdatadir). +Create a issue if you can't find it. + +#### I do not like the order of modules + +All module can be rearranged with drag-n-drop. order is saved in a local db. you can edit this file manually as well, it's a simple json file. #### Do I need to know Rust to get started? -Absolutely not. I don't know Rust myself and I have a complete application that I created from scratch. +Absolutely not. Many modules are written in pure JS, rust is only needed for performance and security sensitive features like calculating hash +or compressing image etc. ## NEED HELP WITH: @@ -122,3 +141,7 @@ Absolutely not. I don't know Rust myself and I have a complete application that ## License [MIT](https://choosealicense.com/licenses/mit/) + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=fosslife/devtools-x&type=Date)](https://star-history.com/#fosslife/devtools-x&Date) diff --git a/features.md b/features.md new file mode 100644 index 0000000..55995ef --- /dev/null +++ b/features.md @@ -0,0 +1,139 @@ +# Features list + +> ignore colors in the videos, it's the compression + +## Inbuilt REST api client (basic) + +https://github.com/fosslife/devtools-x/assets/24642451/8c937aac-a6ec-401b-9209-16f0c4d9e2e8 + +## Epoch + +https://github.com/fosslife/devtools-x/assets/24642451/182cf0da-4237-4373-a9dd-45a470625e37 + +## Ping + +https://github.com/fosslife/devtools-x/assets/24642451/75549e61-edf9-444b-b452-db28d955162e + +## Password Generator + +https://github.com/fosslife/devtools-x/assets/24642451/742f1a5e-688f-4fce-ac91-831cc2707034 + +## QR Code generator + +https://github.com/fosslife/devtools-x/assets/24642451/c29879b4-9bbd-47c2-a4c1-d4f99036f7da + +## Minify/Beautify code + +https://github.com/fosslife/devtools-x/assets/24642451/fc4e4518-4734-433b-96d1-3afacb43cd94 + +## React live scratchpad + +https://github.com/fosslife/devtools-x/assets/24642451/e9236298-54c2-4c67-812e-b873160b6d25 + +## Lorem Ipsum text generator + +https://github.com/fosslife/devtools-x/assets/24642451/c8251476-c1b3-4a5e-9662-b8779969b489 + +## Image compressor with preview + +https://github.com/fosslife/devtools-x/assets/24642451/1989b3ed-156d-4b5c-81f1-ca62dd992e86 + +## Pastebin + +https://github.com/fosslife/devtools-x/assets/24642451/ee5b2f2c-a001-4f28-856f-64d3a98c25eb + +## Programming scratchpad for any language + +https://github.com/fosslife/devtools-x/assets/24642451/5f6c00a7-c3ba-4ca0-925a-c8cbcc536a1f + +## Bulk Image compressor (with SIMD - rust) + +https://github.com/fosslife/devtools-x/assets/24642451/9606e383-ae69-4748-83dc-e208b573b7c2 + +## Base 64 text encoder and decoder + +https://github.com/fosslife/devtools-x/assets/24642451/4f4ec732-6e27-42a8-aa89-2fc6d2c37e1c + +## Base 64 image encoder and decoder + +https://github.com/fosslife/devtools-x/assets/24642451/acbaccd1-b889-48f8-bd00-7d0201672d47 + +## Multi algorithm Hashing text + +https://github.com/fosslife/devtools-x/assets/24642451/3eb6b8b3-339b-4b77-b368-369ef0dbb36f + +## Fast MD5 calculator for files + +https://github.com/fosslife/devtools-x/assets/24642451/c75bf832-c1b5-438f-b62a-186d613fcf95 + +## JSON compressor/formatter, etc tools + +https://github.com/fosslife/devtools-x/assets/24642451/dbcea322-328b-45cd-839e-31db13cd6819 + +## JWT decoder + +https://github.com/fosslife/devtools-x/assets/24642451/c392f72c-8819-42fd-a512-127aa2caef1d + +## Number convertor + +https://github.com/fosslife/devtools-x/assets/24642451/b7be56e0-7ab5-4642-b9be-d32242251bc3 + +## SQL formatter + +https://github.com/fosslife/devtools-x/assets/24642451/1c0e1bb4-0714-430c-ac8f-57c30a1c46ef + +## Color picker/generator/convertor + +https://github.com/fosslife/devtools-x/assets/24642451/21f3ca5d-b846-40cc-8b6f-cddb298045f7 + +## Diff text/code with syntax highlight + +https://github.com/fosslife/devtools-x/assets/24642451/f2fd8f53-fb92-454b-b0bf-34cf5db5675d + +## Markdown editor and live preview + +https://github.com/fosslife/devtools-x/assets/24642451/43301802-a670-4365-8610-bac2fb884852 + +## YAML JSON convertor + +https://github.com/fosslife/devtools-x/assets/24642451/06cfaa56-ba66-453a-9333-b6047bf53ffd + +## Variety of unit convertors + +https://github.com/fosslife/devtools-x/assets/24642451/b97119b2-c4b3-477e-890e-b2c3e4f43eee + +## Text compression gzip/zlib etc + +https://github.com/fosslife/devtools-x/assets/24642451/58efb5e0-2981-4b7f-9e5f-01611f240c46 + +## Stateless password generator + +https://github.com/fosslife/devtools-x/assets/24642451/793df3f1-987c-45f7-8116-95116b328891 + +## Quicktype types generator from sample json + +https://github.com/fosslife/devtools-x/assets/24642451/cc6f213d-ebce-45ed-8065-b4618e045a51 + +## URL parser and visualizer + +https://github.com/fosslife/devtools-x/assets/24642451/39749c42-b839-4c77-899b-609b7877df50 + +## HTML editor and live preview + +https://github.com/fosslife/devtools-x/assets/24642451/9c38b8ae-d240-4fdf-8290-496b3aea29ec + +## PDF reader + +https://github.com/fosslife/devtools-x/assets/24642451/f8438f00-a918-43bf-92ce-7e582ecb06d8 + +## Cron tester, generate english meaning + +https://github.com/fosslife/devtools-x/assets/24642451/ad4ebc4b-0a9d-42b2-abbd-1056438c9504 + +## UUID generator + +https://github.com/fosslife/devtools-x/assets/24642451/8da9a0e3-1916-41bd-a0c2-64a2cdb52a51 + +## Regex playground + +https://github.com/fosslife/devtools-x/assets/24642451/ad64d256-616d-4ca8-909d-9c3a576ed737 diff --git a/package.json b/package.json index 0009dfd..9c68f70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dev-tools", - "version": "2.9.0", + "version": "2.10.0", "license": "MIT", "type": "module", "scripts": { @@ -18,6 +18,7 @@ }, "dependencies": { "@aptabase/tauri": "^0.4.1", + "@faker-js/faker": "^8.4.1", "@hello-pangea/dnd": "^16.6.0", "@loadable/component": "^5.16.3", "@mantine/charts": "^7.7.1", @@ -62,7 +63,7 @@ "react-dom": "^18.2.0", "react-icons": "^5.0.1", "react-live": "^4.1.5", - "react-pdf": "^7.7.1", + "react-pdf": "^7.7.3", "react-router-dom": "6.22.3", "react-shepherd": "^4.3.0", "react-sizeme": "^3.0.2", @@ -71,8 +72,7 @@ "tauri-plugin-store-api": "https://github.com/tauri-apps/tauri-plugin-store", "terser": "^5.30.0", "typescript-eslint": "^7.4.0", - "uuid": "^9.0.1", - "wasm-vips": "^0.0.8" + "uuid": "^9.0.1" }, "devDependencies": { "@actions/github": "^6.0.0", diff --git a/scripts/prepare.mjs b/scripts/prepare.mjs index c2abe13..d08fa13 100644 --- a/scripts/prepare.mjs +++ b/scripts/prepare.mjs @@ -1,16 +1 @@ -import fs from "fs/promises"; - -// copy node_modules/wasm-vips/lib/vips-es6.js to assets/vips-es6.js - -await fs.mkdir("assets/vips", { recursive: true }); - -const vipsEs6 = await fs.readFile("node_modules/wasm-vips/lib/vips.js"); -await fs.writeFile("assets/vips/vips.js", vipsEs6); - -const vipsWasm = await fs.readFile("node_modules/wasm-vips/lib/vips.wasm"); -await fs.writeFile("assets/vips/vips.wasm", vipsWasm); - -const vipsWorker = await fs.readFile( - "node_modules/wasm-vips/lib/vips.worker.js" -); -await fs.writeFile("assets/vips/vips.worker.js", vipsWorker); +console.log("done"); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1811a04..ed2380e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -784,7 +784,7 @@ dependencies = [ [[package]] name = "devtools-x" -version = "2.9.0" +version = "2.10.0" dependencies = [ "anyhow", "base16ct", @@ -1519,9 +1519,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -4195,9 +4195,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tauri" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f078117725e36d55d29fafcbb4b1e909073807ca328ae8deb8c0b3843aac0fed" +checksum = "047aefcc7721bfb8024a9bc39d4719112262610502de7a224fa62c4570cd78d4" dependencies = [ "anyhow", "base64 0.21.7", @@ -4212,7 +4212,7 @@ dependencies = [ "glib", "glob", "gtk", - "heck 0.4.1", + "heck 0.5.0", "http", "ignore", "indexmap 1.9.3", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 15aa32a..9730c23 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "devtools-x" -version = "2.9.0" +version = "2.10.0" description = "Developer tools desktop application" authors = ["Sparkenstein"] license = "MIT" @@ -15,7 +15,7 @@ tauri-build = { version = "1.5.1", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.5.4", features = [ "dialog-ask", "updater", "protocol-all", "http-all", "dialog-open", "dialog-confirm", "clipboard-all", "dialog-save", "fs-all", "devtools"] } +tauri = { version = "1.6.2", features = [ "dialog-ask", "updater", "protocol-all", "http-all", "dialog-open", "dialog-confirm", "clipboard-all", "dialog-save", "fs-all", "devtools"] } tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } md-5 = "0.10.5" base16ct = { version = "0.2.0", features = ["alloc"] } diff --git a/src-tauri/src/commands/image.rs b/src-tauri/src/commands/image.rs index b919a17..fb7a7ed 100644 --- a/src-tauri/src/commands/image.rs +++ b/src-tauri/src/commands/image.rs @@ -1,6 +1,6 @@ pub mod images { use anyhow::Result; - use image::codecs::jpeg::JpegEncoder; + use image::{codecs::jpeg::JpegEncoder, ImageEncoder}; use oxipng::{optimize, InFile, Options, OutFile}; use rayon::prelude::*; use serde::Deserialize; @@ -8,13 +8,14 @@ pub mod images { fs::File, io::{BufWriter, Write}, ops::Deref, + path::Path, }; use tokio::time::Instant; use webp::Encoder as WebPEncoder; pub fn compress(img_path: &String, destination: &String, quality: u8) -> Result<()> { - let path = std::path::Path::new(img_path); - let destination_path = std::path::Path::new(destination); + let path = Path::new(img_path); + let destination_path = Path::new(destination); let img: image::DynamicImage = image::open(path).unwrap(); let width = img.width(); let height = img.height(); @@ -86,7 +87,8 @@ pub mod images { let parent_start = Instant::now(); images.par_iter().for_each(|image| { let start = Instant::now(); - let done = compress(image, &destination, quality); + let done: std::prelude::v1::Result<(), anyhow::Error> = + compress(image, &destination, quality); match done { Ok(_) => { window.emit("image_compressor_progress", image).unwrap(); diff --git a/src-tauri/src/commands/image_compressor.rs b/src-tauri/src/commands/image_compressor.rs new file mode 100644 index 0000000..5e09ee2 --- /dev/null +++ b/src-tauri/src/commands/image_compressor.rs @@ -0,0 +1,74 @@ +pub mod images { + use std::{ops::Deref, path::Path}; + + use image::{ + codecs::{ + jpeg::JpegEncoder, + png::{CompressionType, PngEncoder}, + }, + ColorType, ImageEncoder, + }; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Deserialize, Serialize)] + pub enum ImageFormat { + Jpeg, + Png, + Webp, + } + + #[tauri::command] + pub async fn compress_images_to_buffer( + image_path: String, + quality: u8, + format: ImageFormat, + ) -> Result, String> { + let time = std::time::Instant::now(); + let path = Path::new(&image_path); + + let img = image::open(path).map_err(|e| e.to_string()).unwrap(); + + match format { + ImageFormat::Jpeg => { + let mut writer = Vec::new(); + let mut encoder = JpegEncoder::new_with_quality(&mut writer, quality); + encoder + .encode(img.as_bytes(), img.width(), img.height(), ColorType::Rgb8) + .map_err(|e| e.to_string()) + .unwrap(); + println!("Time: {:?}", time.elapsed()); + return Ok(writer); + } + ImageFormat::Png => { + let mut writer = Vec::new(); + // convert quality percentage to CompressionType + let compression_level = match quality { + 0..=33 => CompressionType::Best, + 34..=66 => CompressionType::Default, + 67..=100 => CompressionType::Fast, + _ => CompressionType::Default, + }; + let encoder = PngEncoder::new_with_quality( + &mut writer, + compression_level, + image::codecs::png::FilterType::Sub, + ); + encoder + .write_image(img.as_bytes(), img.width(), img.height(), ColorType::Rgb8) + .map_err(|e| e.to_string()) + .unwrap(); + println!("Time: {:?}", time.elapsed()); + return Ok(writer); + } + ImageFormat::Webp => { + let x = webp::Encoder::from_image(&img) + .unwrap() + .encode(quality as f32); + // .map_err(|e| e.to_string()) + // .unwrap(); + println!("Time: {:?}", time.elapsed()); + return Ok(x.to_vec()); + } + } + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 5332f94..3b9547a 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -2,5 +2,6 @@ pub mod base64_image; pub mod compress; pub mod hash; pub mod image; +pub mod image_compressor; pub mod minify; pub mod ping; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 26a35a9..6ac8767 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -13,6 +13,7 @@ use commands::base64_image::base64_image::base64_image; use commands::compress::compress::compress; use commands::hash::hash::hash; use commands::image::images::compress_images; +use commands::image_compressor::images::compress_images_to_buffer; use commands::minify::minify::minifyhtml; use commands::ping::ping::ping; @@ -26,7 +27,8 @@ fn main() { minifyhtml, compress_images, base64_image, - compress + compress, + compress_images_to_buffer ]) .setup(|app| { WindowBuilder::new(app, "main", WindowUrl::App("index.html".into())) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2077ff4..dbec559 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "dev-tools", - "version": "2.9.0" + "version": "2.10.0" }, "build": { "distDir": "../dist", diff --git a/src/App.tsx b/src/App.tsx index 1e84206..e7e24fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -70,6 +70,7 @@ const PdfReader = loadable(() => import("./Features/pdf/PdfReader")); const Cron = loadable(() => import("./Features/cron/Cron")); const Ids = loadable(() => import("./Features/ids/Ids")); const Compress = loadable(() => import("./Features/text/TextCompress")); +const Faker = loadable(() => import("./Features/faker/Faker")); const shortCuts = [ { @@ -229,6 +230,7 @@ function App() { }> }> }> + }> diff --git a/src/Features/faker/Faker.tsx b/src/Features/faker/Faker.tsx new file mode 100644 index 0000000..46e04bd --- /dev/null +++ b/src/Features/faker/Faker.tsx @@ -0,0 +1,426 @@ +import classes from "./styles.module.css"; +import { + ActionIcon, + Button, + Group, + ScrollArea, + Select, + Stack, + TextInput, + Divider, + rem, + Text, + NumberInput, +} from "@mantine/core"; +import { FakerInput } from "./FakerInput"; +import { useCallback, useState } from "react"; +import { faker } from "@faker-js/faker"; +import { Monaco } from "../../Components/MonacoWrapper"; +import { + MdOutlineRemove, + MdOutlineClose, + MdOutlineWarning, + MdAdd, +} from "react-icons/md"; +import { notifications } from "@mantine/notifications"; +import YAML from "js-yaml"; + +const errorIcon = ( + +); +const warningIcon = ( + +); + +/** + * Supported output formats + */ +const outputFormats = [ + { format: "json", label: "JSON" }, + { format: "yaml", label: "YAML" }, + { format: "sql", label: "SQL" }, + { format: "csv", label: "CSV" }, +]; + +/** + * Generates a mock data using @faker-js/faker based on given data category and type + * + * @param category The category for which the fake data falls within + * @param dataType The specific data type to be fakes + * @param locale The locale to be used for generating mock data + * @returns The faker generated data + */ +const getMockData = (category: string, dataType: string): any => { + if ( + (faker as any)[category] && + typeof (faker as any)[category][dataType] === "function" + ) { + const op = (faker as any)[category][dataType](); + console.log("generated", op); + if (typeof op === "string") return op; + + return JSON.stringify(op); + } else { + throw new Error(`Invalid category or subset: ${category}.${dataType}`); + } +}; + +interface Field { + fieldName: string; + category: string; + dataType: string; +} + +export default function Faker() { + const [outputFormat, setOutputFormat] = useState("json"); + const [fields, setFields] = useState([ + { fieldName: "id", category: "datatype", dataType: "uuid" }, + ]); + const [tableName, setTableName] = useState("table-1"); + const [rowCount, setRowCount] = useState(10); + const [csvDelimiter, setCsvDelimiter] = useState(","); + const [output, setOutput] = useState(); + + // #region Change Handlers + const tableNameChange = (event: any) => { + setTableName(event.target.value); + }; + + const csvDelimiterChange = (event: any) => { + setCsvDelimiter(event.target.value); + }; + + const fieldNameChange = (index: number, name: string | null) => { + const updatedFields = [...fields]; + updatedFields[index].fieldName = name || ""; + setFields(updatedFields); + }; + + const fieldCategoryChange = (index: number, category: string | null) => { + const updatedFields = [...fields]; + updatedFields[index].category = category || ""; + setFields(updatedFields); + }; + + const fieldDataTypeChange = (index: number, dataType: string | null) => { + const updatedFields = [...fields]; + updatedFields[index].dataType = dataType || ""; + setFields(updatedFields); + }; + // #endregion + + /** + * Adds a new field to the list of fields along with a corresponding FakeInput Compponent + */ + const addField = () => { + setFields([...fields, { fieldName: "", category: "", dataType: "" }]); + }; + + /** + * Removes a specific FakerInput component form view and corresponding Field from list of fields + * + * @param index The index of the field to remove + */ + const removeField = (index: number) => { + setFields(fields.filter((_, i) => i !== index)); + }; + + /** + * Generates the mock data in the desired output + * Determines the output format and uses some helpers and one js-yaml library + */ + const generate = useCallback(async () => { + const result = validateFields(); + if (!result.valid) { + showWarning("Validation Error", result.message); + return; + } + + if (outputFormat === "json" || outputFormat === "yaml") { + let data = []; + for (let i = 0; i < rowCount; i++) { + data.push(objectFromFields(fields)); + } + if (outputFormat !== "yaml") { + let input = JSON.stringify(data, undefined, 2); + setOutput(input); + return; + } + setOutput( + YAML.dump(data, { + indent: 2, + }) + ); + } + if (outputFormat === "csv") { + let output = ""; + for (let i = 0; i < rowCount; i++) { + output += csvRowFromFields(fields, csvDelimiter) + "\n"; + } + setOutput(output); + return; + } + if (outputFormat == "sql") { + let output = ""; + for (let i = 0; i < rowCount; i++) { + output += sqlInsertFromFields(tableName, fields) + "\n"; + } + setOutput(output); + return; + } + }, [rowCount, fields, outputFormat, csvDelimiter, tableName]); + + /** + * Creates a Javscript object with mock property values from a list of field + * + * @param fields + * @returns + */ + const objectFromFields = (fields: Field[]) => { + let obj: { [key: string]: string } = {}; + fields.forEach((f) => { + try { + obj[f.fieldName] = getMockData(f.category, f.dataType); + } catch (error) { + let message; + if (error instanceof Error) message = error.message; + else message = String(error); + showError("Faker Error", message); + } + }); + return obj; + }; + + /** + * Simply quotes a string if it contains the CSV delimeter + * + * @param value The string value to be made safe + * @param delimeter The deleimeter to check for in the string + */ + const csvSafe = (value: string, delimeter: string) => { + if (value.includes(delimeter)) return `"${value}"`; + return value; + }; + + /** + * Creates a CSV row with mock data from a list of fields + * + * @param fields The list of fields to be comma-separated + * @param delimeter Character to distinguish columns + * @returns + */ + const csvRowFromFields = (fields: Field[], delimeter: string) => { + let line: string = ""; + fields.forEach((f) => { + try { + line += `${csvSafe(getMockData(f.category, f.dataType), delimeter)} ${delimeter}`; + } catch (error) { + let message; + if (error instanceof Error) message = error.message; + else message = String(error); + showError("Faker Error", message); + } + }); + return line.slice(0, -1); // remove trailing delimeter + }; + + const sqlInsertFromFields = (tableName: string, fields: Field[]) => { + let obj = objectFromFields(fields); + //array.map(item => `'${item}'`).join(delimiter); + return ` + INSERT INTO ${tableName} (${Object.keys(obj).join(",")}) + VALUES(${Object.values(obj) + .map((item) => `'${item}'`) + .join(",")});`; + }; + + /** + * Checks is a not empty, undefined or null + * + * @param val The string to validate + * @returns boolean result of validation + */ + const validString = (val: string | null) => { + return ![null, undefined, ""].includes(val); + }; + + type ValidResult = { valid: true }; + type InvalidResult = { valid: false; message: string }; // Error message needed only if validation fails + type ValidationResult = ValidResult | InvalidResult; + + /** + * Validate the list of fields + * Check that the list is not empty and that each field's entries are non-empty strings + * + * @returns ValidationResult Result of the validation. It included and error message if validation is false + */ + const validateFields = (): ValidationResult => { + if (fields.length < 1) { + return { + valid: false, + message: "Add at least on field.", + }; + } + + // 3 loops are fine here, need granular control over the error message + for (let i = 0; i < fields.length; i++) { + if (!validString(fields[i].fieldName)) { + return { + valid: false, + message: `Field ${i + 1} is not valid. missing field name.`, + }; + } + } + for (let i = 0; i < fields.length; i++) { + if (!validString(fields[i].category)) { + return { + valid: false, + message: `Field ${i + 1} is not valid. missing category.`, + }; + } + } + + for (let i = 0; i < fields.length; i++) { + if (!validString(fields[i].dataType)) { + return { + valid: false, + message: `Field ${i + 1} is not valid. missing data type.`, + }; + } + } + return { valid: true }; + }; + + /** + * Shows error notification in a toast dialog + * + * @param title The title of the toast + * @param message Message to be included the content of the notification + */ + const showError = (title: string, message: string) => { + notifications.show({ + icon: errorIcon, + title: title, + message: message, + color: "red", + }); + }; + + /** + * Shows warning notification in a toast dialog + * + * @param title The title of the toast + * @param message Message to be included the content of the notification + */ + const showWarning = (title: string, message: string) => { + notifications.show({ + icon: warningIcon, + title: title, + message: message, + color: "orange", + }); + }; + + return ( + + + + + setRowCount(Number(value))} + defaultValue={rowCount} + /> + onCategoryChange(value !== "" ? value : null)} + placeholder="Category" + data={getCategoryNames().map((name) => ({ value: name, label: name }))} + /> +